Skip to content

作用域和闭包

作用域

全局作用域

一个页面就是一个完整的执行环境,里面就存在唯一的一个作用域,就是全局作用域,
全局作用域的本质是全局对象对的属性,在浏览器中全局对象是 window,
在全局环境使用functionvar声明变量或函数相当于为全局对象添加属性或方法

javascript
var a = 2
console.log(a === window.a) //true 直接添加到window对象下成了其属性

函数作用域

除了全局作用域,还有函数作用域。
里面的所有函数只有在函数作用域内才可以调用,
外部不能访问内部函数作用域的变量。

javascript
let a2 = "这是foo外"

function foo() {
    let a1 = "这是foo内"
    console.log(a1, a2)
}
foo() //"这是foo内" "这是foo外"
console.log(a1, a2) //"报错" "这是foo外"

在函数内部可以同时访问 a1,a2
但是在函数外部只能访问 a2,a1 不能访问。
若函数参数实参同名,相当于声明一个同名的局部变量并在执行时赋值

javascript
let num = 2

function foo(num) {
    num += 2
}
foo(4)
console.log(num) //2,没有发生变化

//-----------------------------------------

function foo1() {
    num += 2
}
foo1(4)
console.log(num) //4,发生变化

不同作用域的同名变量不会影响。

声明时上下文

此时有一个全局作用域,里面有 2 个函数作用域如下:
在任何地方都可以访问全局作用域下的变量。

javascript
var a = "a是全局变量"

function foo1() {
    var a = "foo1里面的a"
    console.log(a)
}

function foo2() {
    var a = "foo2里面的a"
    foo1()
}
foo2()

函数的作用域是在函数声明时确定的。在调用时依然是回访声明时的上下文。

es6 块作用域

除了函数可以生成一个函数作用域之外,
代码块 for(){}/while{}的小括号和花括号也可以生成一个块级作用域

javascript
{
    let a = 1
    console.log(a) //1
}
console.log(a) //a is not defined

变量泄漏

functionvar是 es5 遗留的声明方式,他们会无视块作用域
而如果声明不用关键字,则直接写相当于给 window 添加属性或方法

块作用域最常用的地方见本章末尾。

IIFE

IIFE(立即执行的匿名函数表达式)

函数申明的时候要么是立即执行要么是写上函数名,在合适的位置调用。

javascript
//需要将匿名函数用()包裹
;
(function() {
    console.log("ok")
})() //一般写法

//还有别的写法
; + (function() {
    console.log(1)
})() //void和算数运算符也可
;
(function() {
    console.log(1)
})() //()包裹范围变大

//别的函数
void(() => {
    console.log("ok")
})() //箭头函数需要括号
void(function main() {
    console.log("ok")
})() //具名函数直接加名字
void(async function main() {
    console.log(await ajax())
})() //异步函数直接加async

我们之前讲函数的时候讲到过函数加括号表示立即执行。
但是为了不引起歧义需要将函数整体括起来,此时这个函数中形成一个单独的立执行匿名函数作用域,函数名和函数里面的值都不可以访问,可以说整个立执行的区域形成了一个安全区。
比如我们整体将 JS 代码写成一个立执行函数,那么你的 JS 代码就是安全的不可修改的。

垃圾回收机制

JavaScript 是具有自动垃圾收集机制的,无需手动管理:

  • 标记清除(mark-and-sweep)

    JavaScript 最常用的是标记清除:当变量进入环境(作用域),则将变量标记为进入环境,当变量离开环境的时候,将其标记为离开环境,内存被回收。

  • 引用计数(reference counting)

    已经不会被变量引用计数为 0,内存被回收。

如果一个值的引用出现闭环的话,这个值的引用不会变为 0,循环引用使得值的内存永远得不到回收。意味着永远占用内存。

所以:对于不需要的值,或者使用完的值,请手动将变量指向清除。

javascript
var obj = document.getElementById("box")
obj.style.color = "blue"
obj = null //清空指向

引用类型将在后面讲到。

闭包

内部函数能访问外部函数的作用域:

javascript
function foo(c) {
    var num = c
    return function A() {
        num++
        return num
    }
}
foo(5)() //6
foo(5)() //6
foo(5)() //6
var b = foo(5) //b保留function A声明时的上下文
b() //6
b() //7
b() //8
var c = foo(5) //c保留另一份function A声明时的上下文
c() //6
c() //7
c() //8

变量 b 保留下来了一个函数 A 可以访问自己在声明时的上下文(foo 的作用域)里面的变量 num 并对他进行改变。
上面就是一个简单的累加器。

函数可以保留自己的作用域并可以访问它的作用域,就产生了闭包。

闭包的影响: 会使得原有的作用域不消失,导致内存泄漏(可分配的内存的资源减少)。
现代计算机内存足够大,浏览器优化足够好,这个情况几乎不可能发生的。

闭包的作用

封装,私有化属性

javascript
function create() {
    var Gin = {
        money: 100,
    }
    console.log("你的钱是:" + Gin.money)
    return {
        add: function() {
            Gin.money += 10
            console.log("你的钱是:" + Gin.money)
        },
        lost: function() {
            Gin.money -= 5
            console.log("你的钱是:" + Gin.money)
        },
        get: function() {
            return Gin.money
        },
    }
}
var yinshi = create() //"你的钱是:100"
yinshi.add() //"你的钱是:110"
yinshi.lost() //"你的钱是:105"
var yinshi2 = create()
yinshi2.get() //100

在这里 Gin 这个对象完全变成了一个私有对象,里面的值已经无法直接获取,可以通过已经定义好的自定义的方法来访问它。
JavaScript 的闭包不一定要 return。也可以直接为定义更外部的作用域(如 window)的属性。
还有最常见的循环绑定的问题:

javascript
function foo() {
    var arr = []
    for (var i = 0; i < 10; i++) {
        arr[i] = function() {
            console.log(i)
        }
    }
    return arr
}
var mylist = foo() //mylist是一个存了10个函数的数组
//此时mylist数组里的每个函数打印出来都会是10
javascript
function foo() {
    var arr = []
    for (var i = 0; i < 10; i++) {
        arr[i] = (function(j) {
            return function() {
                console.log(j)
            }
        })(i)
    }
    return arr
}
var mylist = foo()
//此时mylist数组里的每个函数打印出来就是对应的i的值

特别的,利用 let 具有不穿透块作用域的性质,会被 for 的花括号限制:

javascript
function foo() {
    var arr = []
    for (let i = 0; i < 10; i++) {
        arr[i] = function() {
            console.log(i)
        }
    }
    return arr
}
var mylist = foo() //mylist是一个存了10个函数的数组
//无需闭包,此时mylist数组里的每个函数打印出来就是对应的i的值

在 for 循环中使用定时器(等其他异步代码)的时候和这个上述的循环绑定事件类似,
为了达到预期效果需要使用闭包或者在 for 循环中使用let