作者:潇湘客老师
闭包是通过在对一个函数调用的执行环境中返回一个函数对象构成的。比如,在对函数调用的过程中,将一个对内部函数对象的引用指定给另一个对象的属性。或者,直接将这样一个(内部)函数对象的引用指定给一个全局变量、或者一个全局性对象的属性,或者一个作为参数以引用方式传递给外部函数的对象。例如:-
function exampleClosureForm(arg1, arg2){
var localVar = 8;
function exampleReturned(innerArg){
return ((arg1 + arg2)/(innerArg + localVar));
}
/* 返回一个定义为 exampleReturned 的内部函数的引用 -:- */ return exampleReturned;
}
var globalVar = exampleClosureForm(2, 4);
这种情况下,在调用外部函数 exampleClosureForm 的执行环境中所创建的函数对象就不会被当作垃圾收集,因为该函数对象被一个全局变量所引用,而且仍然是可以访问的,甚至可以通过 globalVar(n) 来执行。
的确,情况比正常的时候要复杂一些。因为现在这个被变量 globalVar 引用的内部函数对象的 [[scope]] 属性所引用的作用域链中,包含着属于创建该内部函数对象的执行环境的活动对象(和全局对象)。由于在执行被 globalVar 引用的函数对象时,每次都要把该函数对象的 [[scope]] 属性所引用的整个作用域链添加到创建的(内部函数的)执行环境的作用域中(即此时的作用域中包括:内部执行环境的活动对象、外部执行环境的活动对象、全局对象。译者注), 所以这个(外部执行环境的)活动对象不会被当作垃圾收集。
闭包因此而构成。此时,内部函数对象拥有自由的变量,而位于该函数作用域链中的活动(可变)对象则成为与变量绑定的环境。
由于活动(可变)对象受限于内部函数对象(现在被 globalVar 变量引用)的 [[scope]] 属性中作用域链的引用,所以活动对象连同它的变量声明--即属性的值,都会被保留。而在对内部函数调用的执行环境中进行作用域解析时,将会把与活动(可变)对象的命名属性一致的标识符作为该对象的属性来解析。活动对象的这些属性值即使是在创建它的执行环境退出后,仍然可以被读取和设置。
在上面的例子中,当外部函数返回(退出它的执行环境)时,其活动(可变)对象的变量声明中记录了形式参数、内部函数定义以及局部变量的值。arg1 属性的值为 2,而 arg2 属性的值为 4,localVar 的值是 8,还有一个 exampleReturned 属性,它引用由外部函数返回的内部函数对象。(为方便起见,我们将在后面的讨论中,称这个活动<可变>对象为 "ActOuter1")。
如果再次调用 exampleClosureForm 函数,如:-
var secondGlobalVar = exampleClosureForm(12, 3);
- 则会创建一个新的执行环境和一个新的活动对象。而且,会返回一个新的函数对象,该函数对象的 [[scope]] 属性引用的作用域链与前一次不同,因为这一次的作用域链中包含着第二个执行环境的活动对象,而这个活动对象的属性 arg1 值为 12 而属性 arg2 值为 3。(为方便起见,我们将在后面的讨论中,称这个活动<可变>对象为 "ActOuter2")。
通过第二次执行 exampleClosureForm 函数,第二个、也是截然不同的闭包诞生了。
通过执行 exampleClosureForm 创建的两个函数对象分别被指定给了全局变量 globalVar 和 secondGlobalVar,并返回了表达式 ((arg1 + arg2)/(innerArg + localVar))。该表达式对其中的四个标识符应用了不同的操作符。如何确定这些标识符的值是体现闭包价值的关键所在。
我们来看一看,在执行由 globalVar 引用的函数对象--如 globalVar(2)--时的情形。此时,会创建一个新的执行环境和相应的活动对象(我们将称之为“ActInner1”),并把该活动对象添加到执行的函数对象的 [[scope]] 属性所引用的作用域链的前端。ActInner1 会带有一个属性 innerArg,根据传递的形式参数,其值被指定为 2。这个新执行环境的作用域链变成: ActInner1->ActOuter1->全局对象.
为了返回表达式 ((arg1 + arg2)/(innerArg + localVar)) 的值,要沿着作用域链进行标识符解析。表达式中标识符的值将通过依次查找作用域链中每个对象(与标识符名称一致)的属性来确定。
作用域链中的第一个对象是 ActInner1,它有一个名为 innerArg 的属性,值是 2。所有其他三个标识符在 ActOuter1 中都有对应的属性:arg1 是 2,arg2 是 4 而 localVar 是 8。最后,函数调用返回 ((2 + 2)/(2 + 8))。
现在再来看一看由 secondGlobalVar 引用的同一个函数对象的执行情况,比如 secondGlobalVar(5)。我们把这次创建的新执行环境的活动对象称为 “ActInner2”,相应的作用域链就变成了:ActInner2->ActOuter2->全局对象。ActInner2 返回 innerArg 的值 5,而 ActOuter2 分别返回 arg1、arg2 和 localVar 的值 12、3 和 8。函数调用返回的值就是 ((12 + 3)/(5 + 8))。
如果再执行一次 secondGlobalVar,则又会有一个新活动对象被添加到作用域链的前端,但 ActOuter2 仍然是链中的第二个对象,而他的命名属性会再次用于完成标识符 arg1、arg2 和 localVar 的解析。
这就是 ECMAScript 的内部函数获取、维持和访问创建他们的执行环境的形式参数、声明的内部函数以及局部变量的过程。这个过程说明了构成闭包以后,内部的函数对象在其存续过程中,如何维持对这些值的引用、如何对这些值进行读取的机制。即,创建内部函数对象的执行环境的活动(可变)对象,会保留在该函数对象的 [[scope]] 属性所引用的作用域链中。直到所有对这个内部函数的引用被释放,这个函数对象才会成为垃圾收集的目标(连同它的作用域链中任何不再需要的对象)。
内部函数自身也可能有内部函数。在通过函数执行返回内部函数构成闭包以后,相应的闭包自身也可能会返回内部函数从而构成它们自己的闭包。每次作用域链嵌套,都会增加由创建内部函数对象的执行环境引发的新活动对象。ECMAScript 规范要求作用域链是临时性的,但对作用域链的长度却没有加以限制。在具体实现中,可能会存在实际的限制,但还没有发现有具体限制数量的报告。目前来看,嵌套的内部函数所拥有的潜能,仍然超出了使用它们的人的想像能力。
UID50579 帖子297 精华0 下载币114 枚 金币236 枚 阅读权限200 在线时间131 小时 注册时间2008-10-2 最后登录2008-12-22 查看个人网站
查看详细资料
引用 使用道具 报告 回复 TOP