闭包是 JavaScript 中最强大的抽象概念之一,但它也是最容易造成困惑的。它究竟是做什么的呢?
function makeAdder(a) {
return function(b) {
return a + b;
}
}
var add5 = makeAdder(5);
var add20 = makeAdder(20);
add5(6); // ?
add20(7); // ?
makeAdder
这个名字本身,便应该能说明函数是用来做什么的:它会用一个参数来创建一个新的“adder”函数,再用另一个参数来调用被创建的函数时,makeAdder
会将一前一后两个参数相加。
从被创建的函数的视角来看的话,这两个参数的来源问题会更显而易见:新函数自带一个参数——在新函数被创建时,便钦定、钦点了前一个参数(如上方代码中的 a、5 和 20,参考 makeAdder
的结构,它应当位于新函数外部);新函数被调用时,又接收了后一个参数(如上方代码中的 b、6 和 7,位于新函数内部)。最终,新函数被调用的时候,前一个参数便会和由外层函数传入的后一个参数相加。
这里发生的事情和前面介绍过的内嵌函数十分相似:一个函数被定义在了另外一个函数的内部,内部函数可以访问外部函数的变量。唯一的不同是,外部函数已经返回了,那么常识告诉我们局部变量“应该”不再存在。但是它们却仍然存在——否则 adder
函数将不能工作。也就是说,这里存在 makeAdder
的局部变量的两个不同的“副本”——一个是 a
等于 5,另一个是 a
等于 20。那些函数的运行结果就如下所示:
add5(6); // 返回 11
add20(7); // 返回 27
下面来说说,到底发生了什么了不得的事情。每当 JavaScript 执行一个函数时,都会创建一个作用域对象(scope object),用来保存在这个函数中创建的局部变量。它使用一切被传入函数的变量进行初始化(初始化后,它包含一切被传入函数的变量)。这与那些保存的所有全局变量和函数的全局对象(global object)相类似,但仍有一些很重要的区别:第一,每次函数被执行的时候,就会创建一个新的,特定的作用域对象;第二,与全局对象(如浏览器的 window
对象)不同的是,你不能从 JavaScript 代码中直接访问作用域对象,也没有可以遍历当前作用域对象中的属性 的方法。
所以,当调用 makeAdder
时,解释器创建了一个作用域对象,它带有一个属性:a
,这个属性被当作参数传入 makeAdder
函数。然后 makeAdder
返回一个新创建的函数(暂记为 adder
)。通常,JavaScript 的垃圾回收器会在这时回收 makeAdder
创建的作用域对象(暂记为 b
),但是,makeAdder
的返回值,新函数 adder
,拥有一个指向作用域对象 b
的引用。最终,作用域对象 b
不会被垃圾回收器回收,直到没有任何引用指向新函数 adder
。
作用域对象组成了一个名为作用域链(scope chain)的(调用)链。它和 JavaScript 的对象系统使用的原型(prototype)链相类似。
一个闭包,就是 一个函数 与其 被创建时所带有的作用域对象 的组合。闭包允许你保存状态——所以,它们可以用来代替对象。