闭包
JavaScript 闭包无处不在,你只需要能够识别并拥抱它。
闭包的实质
定义
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即函数是在当前词法作用域之外执行。
function foo() {
var a = 2
function bar() {
console.log(a) // 2
}
bar()
}
foo()
在上述代码中,函数 bar()
具有一个覆盖 foo()
作用域的闭包(事实上覆盖了它能访问的所有作用域)。
原理
为了更加清晰地观察闭包是如何工作的,我们对代码进行一些修改。
function foo() {
var a = 2
function bar() {
console.log(a)
}
return bar
}
var baz = foo()
baz() // 2 —— 这就是闭包的作用
在这个例子中,我们将函数 bar
所引用的函数对象本身当做返回值。
在 foo()
执行后,其返回值(也就是内部的 bar
)赋值给 baz
并调用 baz()
,实际上就是通过不同的标识符引用调用了内部的函数 bar()
。
因为引擎具有垃圾回收机制来释放不再使用的内存空间,所以通常来说,当 foo()
执行完毕后,foo()
的整个内部作用域都会被销毁。
而闭包会阻止这件事情的发生。因为 bar()
拥有涵盖 foo()
内部作用域的闭包,使得该作用域能够一直存活,以供 bar()
在之后的任何时间进行引用。这个引用就叫做闭包。
这个函数在定义时的词法作用域以外的地方被调用,闭包使得函数可以继续访问定义时的词法作用域。
循环和闭包
for循环是闭包的常见例子。
for (var i = 0; i <= 5; i++) {
setTimeout( function timer() {
console.log(i)
}, i*1000)
}
预期的输出结果分别是1-5,每秒一次。
但是实际的输出结果是以每秒一次的频率输出五个6。
对上面的代码进行分析:这个循环的终止条件是 i 不再 <=5,条件首次成立的情况是6,因此输出显示的是循环结束时 i 的最终值6。
深入分析造成这种缺陷的原因,循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i 。而我们的理想情况是循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。
现在开始解决这个问题。
方法一:我们需要在循环过程中的每个迭代中创建一个闭包作用域。
for (var i = 0; i <= 5; i++) {
(function(j) {
setTimeout( function timer() {
console.log(j)
}, j*1000)
})(i)
}
利用立即执行函数,我们将 i 传递进去。
在迭代内使用立即执行函数,会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代都会含有一个具有正确值的变量供我们使用。
方法二:使用块级作用域,let声明变量。
for (let i = 0; i <= 5; i++) {
setTimeout( function timer() {
console.log(i)
}, i*1000)
}
for循环中的let声明,指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
模块中的闭包
最常见的实现模块的方法通常被称为 模块暴露
,下面的代码展示其变体:
function CoolModule() {
var something = "cool"
var another = [1, 2, 3]
function doSomething() {
console.log(something)
}
function doAnother() {
console.log(another.join("!"))
}
return {
doSomething,
doAnother
}
}
var foo = CoolModule()
foo.doSomething() // cool
foo.doAnother() // 1!2!3
分析以上代码:
- 需要通过调用
CoolModule()
来创建一个模板实例。如果不执行外部函数,内部作用域和闭包都无法被创建。 CoolModule()
返回一个对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。可以将这个对象类型的返回值看做本质上是模块的公共API。doSomething()
和doAnother()
函数具有涵盖模块实例内部作用域的闭包。
我们可以将模块函数转换为IIFE,立即调用这个函数并将返回值直接复制给单利的模块实例标识符 foo
。
var foo = (function CoolModule() {
var something = "cool"
var another = [1, 2, 3]
function doSomething() {
console.log(something)
}
function doAnother() {
console.log(another.join("!"))
}
return {
doSomething,
doAnother
}
})()
foo.doSomething()
foo.doAnother()
参阅
《你不知道的JavaScript》
小结
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
模块有两个特征:
- 为创建内部作用域而调用了一个包装函数;
- 包装函数的返回值必须至少包括一对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。