JavaScript 闭包原理、作用域链与内存管理解析

作者:木棉

时间:2026-10-27

浏览:666

本文深入解析 JavaScript 闭包的底层原理,阐述其基于词法作用域的工作机制。通过对比 C# 等编译型语言,说明局部变量在堆上的保存方式。文章结合代码示例,分析闭包在避免全局污染和内存泄漏方面的应用,并探讨循环引用及宿主对象交互导致的内存问题。

文章目录

JavaScript 闭包原理与机制

闭包机制常被讨论为既复杂又直观的概念,以下将结合个人理解进行阐述。

在函数式语言中,函数能够保留内部数据状态。相比之下,C# 等编译型命令式语言由于代码在只读的代码段中执行,函数内的数据仅限于静态数据。局部变量存储在栈上,函数执行完毕后栈空间释放,因此无法持久保存局部变量。

JavaScript 采用词法作用域,函数的执行依赖于定义时确定的变量作用域。因此,JavaScript 中的函数对象不仅包含代码逻辑,还引用当前的作用域链。函数内部的局部变量可被修改,且再次进入函数时,之前修改的状态得以保留。这是因为局部变量并非存储在栈上,而是通过一个对象来保存。

变量的解析由作用域链决定。每次生成函数实例时,都会创建一个对象来保存局部变量,并将其加入作用域链。不同的函数对象可以通过作用域链相互关联。在 JavaScript 中,所有函数本质上都是闭包,我们无法避免“产生”闭包。

引用一张《JavaScript 高级程序设计》中的示意图(见 )来说明,尽管该图并未涵盖所有情况。图中的 activation object 即为用于保存变量的对象。

JavaScript 闭包原理与机制

 

简而言之,在 JavaScript 中:

闭包是指函数实例保存执行时所需变量的引用,而非复制保存当时变量的值。(在 Object C 的实现中,可以选择保存当时的值或引用)。

作用域链是解析变量时查找变量所在的方式,以 var 声明为终止符号;如果链上一直未找到 var,则一直追溯到全局对象为止。

C# 中的闭包特性是由编译器将局部变量转换为引用类型的对象成员来实现的。

闭包的实际应用与示例

接下来通过具体例子说明如何利用闭包这一特性:

1. 闭包是在定义时产生的

function Foo(){ function A(){} function B(){} function C(){}} 每次执行 Foo() 时,都会产生 A、B、C 这三个函数实例(闭包)。当 Foo 执行完毕,若生成的实例没有其他引用,它们将被视为垃圾并随之销毁(不一定是立即销毁)。 我们来证实一下作用域链是在函数定义时确定的,因此这里显示的应该是 'local scope'。

var scope = "global scope"; function checkscope() { var scope = "local scope"; function f() { return scope; } return f;}checkscope()()

同样道理:

(function(){ function A(){} function B(){} function C(){}}()) 上述表达式执行完后也会产生 A、B、C 这三个函数实例(闭包)。因为这是一个立即执行的匿名函数,这三个闭包只能产生一次。生成的闭包若无其他引用,将被视为垃圾并随之销毁(不一定是立即销毁)。

我们之所以这样编写,目的有两个:

1. 避免污染全局对象

2. 避免多次产生相同的函数实例

 

对比下面两个例子,观察闭包是如何保存作用域链的:

function A(){} //比较省内存的写法,创建对象速度快,开销小 (function(prototype){ var name = "a"; function sayName () { alert(name); } function ChangeName() { name += "_changed" } prototype.sayName = sayName;//引用通过执行匿名函数产生的闭包,闭包只会产生一次 prototype.changeName = ChangeName; }(A.prototype)) var a1 = new A(); var a2 = new A(); a1.sayName(); a1.changeName(); a2.sayName();


--------------------------------------------------------------------------------

function B(){ //原型链比较短的做法,找到方法的速度快,但是比较耗内存,每次new 调用构造器都有2个函数实例和1个变量产生。 var name = "b"; function sayName () { alert(name); } function changeName() { name += "_changed"; } this.sayName = sayName;//引用闭包,每次调用函数B都会产生新的闭包 this.changeName = ChangeName; }//如果函数调用之前带有new关键字,则函数作为构造器使用。//本质上来说作为构造器和作为普通函数调用没区别。如果直接调用B(),那么this对象会绑定到全局对象,新生成的闭包会代替旧的闭包赋给全局对象的changeName和sayName属性上,因此旧的闭包会被当成垃圾回收。//如果作为构造器使用,new 关键字会生成一个新的对象(this指向这个新对象)并初始化这个新对象的sayName和changeName属性,因此每次生成的闭包都会因为有引用而保留下来。 var b1 = new B(); b1.sayName(); b1.changeName(); b1.sayName(); var b2 = new B(); b2.sayName(); b1.sayName();

闭包引发的内存泄漏问题

三、泄漏问题:在编译语言中,函数体总在文件的代码段中,并在运行期被装入标志为可执行的内存区。事实上我们不认为函数自身会有生命周期。我们在大多数情况下会认为“引用类型的数据结构”具有生存周期和泄漏的问题,如指针、对象等。 JavaScript 中内存的泄漏本质上就是定义函数时生成的保存局部变量的对象因为存在引用而不被当成垃圾被回收。

1. 存在循环引用

2. 有些对象总不能销毁,如 IE6 在 DOM 中的内存泄漏,或者在销毁时不能通知到 Javascript 引擎,因此也就有些 Javascript 闭包总不能被销毁。这些情况通常是发生在 Javascript 宿主对象和 Javascript 中原生对象沟通不畅导致。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章