好吧,在调用foo
完成后,在调用期间创建的所有内容都符合垃圾回收 (GC) 条件,因为该代码中的任何内容都没有保留在调用期间创建的任何内容。更有趣的问题是如果foo
返回 bar
(函数,而不是bar()
调用产生的数字bar
)会发生什么。
但是使用您拥有的代码,这是您调用时发生的情况的理论foo
(在规范的§10.4.3中定义):
引擎创建了一个新的声明性环境,它最初是用于特定调用的词法环境和变量环境foo
(通常它们不会分开;with
关键字可以将它们分开,但大多数人不使用它)。该声明性环境具有与之关联的绑定对象。
任何声明的参数foo
,名称,声明中的foo
任何变量,通过函数声明声明的任何函数的名称,以及其他一些东西(以定义的顺序)被创建为该绑定对象的属性(详细信息见第 10.5 节) .foo
var
创建bar
函数的过程(在§13.2中描述)将调用的词法环境附加foo
到bar
函数作为其[[Scope]]
属性(不是您可以在代码中使用的文字名称,而是在规范中使用的名称)。
x
绑定对象(例如,x
变量)的属性获取值10
。
调用bar
创建一个全新的声明性环境等,使用y
变量。新环境的绑定对象具有指向创建它的环境的绑定对象的链接。该环境将bar
'[[Scope]]
属性作为其外部词法环境引用。
y
最里面的绑定对象的属性获取值20
。
表达式x + y
被评估:
引擎尝试解析x
以获得它的值。首先,它查看最里面的绑定对象,看看它是否有一个名为 的属性x
,但它没有。
引擎转到当前的外部词法环境,看看它是否在其绑定对象上有一个x
属性。既然这样做了,引擎就会读取属性的值并在表达式中使用它。
引擎尝试解析y
以获得它的值。首先,它查看最里面的绑定对象,看看它是否有一个名为y
;的属性。确实如此,因此引擎将该值用于表达式。
引擎通过添加20
to 来完成表达式10
,将结果压入堆栈,然后返回 out of bar
。
此时,bar
可以通过 GC 回收调用的环境和绑定对象。
引擎从 中获取返回值bar
,将其压入堆栈,然后从 中返回foo
。
此时,foo
可以通过 GC 回收调用的环境和绑定对象。
代码调用console.log
结果。(细节省略。)
所以理论上,没有持久的记忆影响。环境及其绑定对象可以被扔掉。
现在,事实上,现代 JavaScript 引擎非常聪明,并且使用堆栈进行某些对象分配,因此它们不必调用 GC 来回收这些环境和绑定对象。(但请继续阅读。)
现在,假设foo
看起来像这样:
function foo() {
var x = 10;
function bar() {
var y = 20;
return x + y;
}
return bar;
}
我们这样做了:
var b = foo();
现在,foo
返回一个引用bar
(不调用它)。
上面的步骤 1-4 没有改变,但不是调用 bar
,而是foo
返回对它的引用。这意味着通过调用创建的环境和绑定对象foo
不符合 GC 条件,因为bar
在该调用期间创建的函数具有对它们的引用,并且我们具有对该函数的引用(通过b
变量)。所以理论上在那个时候,堆上存在这样的东西:
+-----+ +-------------+
| b |---->| 功能 |
+-----+ +-------------+
| 名称:“酒吧” | +----------------+
| [[范围]] |---->| 环境 |
+-------------+ +----------------+ +---------+
| 绑定对象 |---->| x: 10 |
+----------------+ +--------+
因此,如果现代引擎能够巧妙地将这些对象分配到堆栈上(有时),那么它们在foo
返回后如何仍然存在?您必须深入研究各个引擎的内部结构才能确定。有些人可能会执行静态分析以查看这种情况是否可能,如果绑定对象可以存活,则从一开始就使用堆分配。有些人可能只是确定何时foo
返回应该存活的内容并将这些内容从堆栈复制到堆中。或者 [在此处插入非常聪明的编译器编写者的东西]。一些引擎可能足够聪明,只保留可能被引用的东西(因此,如果您的变量foo
从未以任何方式被 引用bar
,它们可能会从绑定对象中删除)。高级别的,规范要求它看起来就像上面的结构保留在内存中一样,我们无法在代码中做任何事情来证明这不是发生的事情。
如果我们然后调用b
,我们将继续执行上面的步骤,执行步骤 5 到 10,但是当b
返回时,上面的结构继续存在。
这就是 JavaScript闭包的工作方式。