如果我可以提供一个模型来说明闭包何时以及如何创建(这个讨论是理论性的,在现实中,解释器可能会做任何事情,只要最终结果相同):每当在执行期间评估函数时,都会创建一个闭包。然后,闭包将指向执行发生的环境。当网站加载时,Javascript 在全局环境中按从上到下的顺序执行。所有出现的情况都是闭包。
function f(<vars>) {
<body>
}
将被转化为一个闭包,并带有和,指向全局环境的指针。同时,在全局环境中创建一个指向此闭包的引用f
。
那么当在全局环境执行f()
时会发生什么呢?我们可以将其想象为首先在全局环境(函数正在执行的地方)中查找名称f
。我们发现它指向一个闭包。要执行该闭包,我们创建一个新环境,其父环境是由闭包f
指向的环境,即全局环境。在这个新环境中,我们将f
的参数与其实际值关联起来。然后在新环境中执行闭包f
的主体!任何f
所需的变量都将优先在我们刚刚创建的新环境中解析。如果不存在这样的变量,则我们递归地在父环境中查找,直到我们找到全局环境。任何f
创建的变量都将在新环境中创建。
现在,让我们来看一个更复杂的例子:
var i = 10;
function make_counter(start) {
return function() {
var value = start++;
return value;
};
}
var count = make_counter(10);
count();
count();
count = 0;
发生的情况是:
在点(1):在全局环境中建立了从i到10
的关联(执行var i = 10;
)。
在点(2):使用变量(start)
和主体return ...;
创建了一个闭包,该闭包指向正在执行的环境(全局)。然后,将make_counter
与我们刚刚创建的闭包建立了关联。
在第三步骤中,发生了几件有趣的事情。首先,我们找到了全局环境中与
make_counter
相关联的内容。然后我们执行该闭包。因此,创建了一个新的环境,让我们称其为
CE
,它指向由闭包
make_counter
(全局)指向的环境。然后,在
CE
中创建了从
start
到
10
的关联,并在
CE
中运行闭包
make_counter
的主体。在这里,我们遇到了另一个函数,它是匿名的。但是,发生的事情与之前相同(回想一下
function f() {}
等价于
var f = function() {};
)。创建了一个闭包,让我们称其为
count
,其中变量
()
(空列表)和主体
var ... return value;
。现在,这个闭包将指向它正在执行的环境,即
CE
。这将在以后非常重要。最后,我们让
count
指向全局环境中的新闭包(为什么是全局?因为
var count ...
在全局环境中执行)。我们注意到,
CE
没有被垃圾回收,因为我们可以通过变量
make_counter
从全局环境中访问闭包
make_counter
,从而访问
CE
。
在第4点,更有趣的事情发生了。我们首先找到与
count
相关联的闭包,这是我们刚刚创建的闭包。然后我们创建一个新的环境,其父级是闭包指向的环境,即
CE
!我们在这个新环境中执行闭包的主体。当执行
var value = start++;
时,我们从当前环境开始向上移动,顺序查找变量
start
,直到全局环境为止。我们在环境
CE
中找到了
start
。我们将此
start
的值从最初的
10
增加到
11
。现在,在
CE
中的
start
指向值
11
。当我们遇到
var value
时,这意味着不要费劲寻找现有的
value
,只需在执行它的环境中创建一个变量。因此,建立了从
value
到
11
的关联。在
return value;
中,我们以与查找
start
相同的方式查找
value
。结果发现它在当前环境中,因此我们不需要查找父级环境。然后我们返回这个值。现在,我们刚刚创建的新环境将被垃圾收集,因为我们无法通过任何路径从全局访问此环境。
在第5点,与上面发生了相同的事情。但是现在,当我们寻找start
时,我们发现值为11
而不是10
(在环境CE
中)。
在第6点,我们在全局环境中重新分配count
。我们发现现在我们无法再从全局到闭包count
找到路径,因此我们也无法再找到通向环境CE
的路径。因此,这两者都将被垃圾回收。
P.S. 对于熟悉LISP或Scheme的人来说,上面的模型与LISP / Scheme中的环境模型完全相同。
P.P.S. 哇,起初我想写一个简短的答案,但结果变成了这个庞然大物。我希望我没有犯什么明显的错误。