以前对闭包的理解总是含糊不清,最近了解了 JavaScript 的内存模型,发现从内存模型中去理解闭包会是非常简单。
理解闭包需要有作用域和变量对象的概念,在之前的文章有提到:JavaScript-作用域链,JavaScript-执行环境。
先看如下代码:
1 | function foo() { |
这个例子中,bar
函数就是闭包。
这段代码的运行分成以下几步:
1、执行全局执行环境,创建并运行 foo
函数:
1 | ECStack = [ |
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 | var closureA |
bar
函数和 baz
函数的 [[Scope]] 都包含了 foo
函数的变量对象,所以如果其中一个函数修改了 [[Scope]] 中包含的变量的值,也会影响到其他的函数。
总结
理解闭包的关键是理解函数的作用域链和变量对象。
闭包过多会占用过多的内存,必要时需要清除掉不再使用的闭包。
引用了相同变量对象的闭包会相互影响。
可以说,闭包无处不在,就如汤姆大叔的文章中所说:
因为作用域链,使得所有的函数都是闭包,
只有一类函数除外,那就是通过 Function 构造器创建的函数,因为其 [[Scope]] 只包含全局对象。
参考
深入理解 JavaScript 系列(16):闭包(Closures)
《JavaScript 高级程序设计》
JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述!
JavaScript 是如何工作的:JavaScript 的内存模型