以前对闭包的理解总是含糊不清,最近了解了 JavaScript 的内存模型,发现从内存模型中去理解闭包会是非常简单。

理解闭包需要有作用域和变量对象的概念,在之前的文章有提到:JavaScript-作用域链JavaScript-执行环境

先看如下代码:

1
2
3
4
5
6
7
8
9
function foo() {
var a = 1
return function bar() {
console.log(a)
}
}

var closure = foo()
closure() // 1

这个例子中,bar 函数就是闭包。

这段代码的运行分成以下几步:

1、执行全局执行环境,创建并运行 foo 函数:

1
2
3
4
ECStack = [
fooContext,
globalContext
]

2、执行 foo 函数的执行环境时,创建并返回 bar 函数,此时 foo 函数的变量标识符被保存到 bar 函数内部的 [[Scope]] 属性中

3、bar 函数被作为返回值,赋值给了 closure 变量,此时 bar 保存在堆中,由于 bar 被引用中,所以不会被垃圾回收(GC)销毁:

4、执行 bar 函数,先从 bar 函数的变量对象开始,再到 [[Scope]] 属性搜索标识符,在 [[Scope]] 中搜索到了 a 标识符,并打印 a 的值。

理解闭包关键的地方就在于函数在创建阶段,会把父变量对象保存到 [[Scope]] 内部属性中,当函数被引用的时候,垃圾回收机制不会将这个函数回收,并且其父变量对象也不会被回收,当执行这个函数时,会从自身的变量对象中开始到内部变量 [[Scope]] 搜索变量标识符。

由于函数的 [[Scope]] 内部属性会一直存在,直到被销毁,闭包过多,会占用过多的内存,如果确切地不需要再使用闭包,可以把闭包赋值为 null,垃圾收集就会把闭包销毁掉:

1
closure = null

两个引用了相同变量对象的闭包会造成相互影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var closureA
var closureB

function foo() {
var a = 2

closureA = function bar() {
a++
}
closureB = function baz() {
console.log(a)
}
}

foo()
closureA()
closureB() // 3

bar 函数和 baz 函数的 [[Scope]] 都包含了 foo 函数的变量对象,所以如果其中一个函数修改了 [[Scope]] 中包含的变量的值,也会影响到其他的函数。

总结

理解闭包的关键是理解函数的作用域链和变量对象。

闭包过多会占用过多的内存,必要时需要清除掉不再使用的闭包。

引用了相同变量对象的闭包会相互影响。

可以说,闭包无处不在,就如汤姆大叔的文章中所说:

因为作用域链,使得所有的函数都是闭包,
只有一类函数除外,那就是通过 Function 构造器创建的函数,因为其 [[Scope]] 只包含全局对象。

参考

深入理解 JavaScript 系列(16):闭包(Closures)

《JavaScript 高级程序设计》

JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述!

JavaScript 是如何工作的:JavaScript 的内存模型

并发模型与事件循环