JavaScript 闭包深入理解:从定义到垃圾回收机制
本文深入解析 JavaScript 闭包的定义、内部机制及应用场景。通过剖析函数定义与执行时的作用域链变化、活动对象及引用关系,阐明闭包如何保护变量安全并维持内存状态。同时结合垃圾回收(GC)原理,解释闭包导致内存占用的原因及优化建议,帮助开发者写出更安全、优雅的代码。
一、什么是闭包?
官方对闭包的描述是:它包含许多变量及绑定这些变量的环境表达式(通常是函数),这些变量也是表达式的一部分。这一定义较为学术,通俗来说,JavaScript 中所有的函数本质上都是闭包。不过,通常我们指的“闭包”更多是指嵌套函数所产生的强大闭包。参考以下代码示例:
function a() {
var i = 0;
function b() {
alert(++i);
}
return b;
}
var c = a();
c();该代码具备两个显著特征:
- 函数 b 嵌套定义在函数 a 内部;
- 函数 a 的返回值是函数 b。
其引用关系如图所示:

执行 var c=a() 后,变量 c 指向了函数 b。由于 b 引用了变量 i,再次执行 c() 会弹出窗口显示 i 的值(首次为 1)。这段代码创建了一个闭包,原因是函数 a 外部的变量 c 引用了函数 a 内部的函数 b。
当函数 a 的内部函数 b 被函数 a 外部的变量引用时,便形成了通常所说的“闭包”。
更透彻地讲,闭包是在构造函数体内定义目标对象的方法函数,而该方法函数反过来引用外层函数体中的临时变量。这使得只要目标对象存在,就能通过其方法间接保持原构造函数调用时的临时变量值。即使构造函数调用结束,临时变量名消失,目标对象的方法仍能访问该值且仅能通过此方式访问。再次调用相同构造函数会生成新对象、方法及临时变量,彼此独立。
为了更深入理解闭包,接下来探讨其作用与效果。
二、闭包有什么作用和效果?
简言之,闭包的作用在于:当函数 a 执行并返回后,JavaScript 的垃圾回收机制(GC)不会回收 a 占用的资源,因为内部函数 b 的执行依赖于 a 中的变量。这是对闭包作用的直观描述,虽不严谨但易于理解,深入理解需循序渐进。在上述例子中,由于闭包存在,函数 a 返回后,a 中的 i 始终存在,每次执行 c(),i 都会自增 1 后弹出。
若 a 返回的不是函数 b,情况则完全不同。a 执行完后,b 未被返回给外部,仅被 a 引用;此时 a 仅被 b 引用,两者互相引用但不被外界引用,因此函数 a 和 b 会被 GC 回收(关于 JavaScript 垃圾回收机制将在后文详述)。
三、闭包的微观世界
要深入了解闭包及函数 a 与嵌套函数 b 的关系,需引入几个概念:函数的执行环境(execution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数 a 从定义到执行的过程为例阐述这些概念。
- 定义函数 a 时,JavaScript 解释器将函数 a 的作用域链(scope chain)设置为定义 a 时所在的“环境”。若 a 是全局函数,则 scope chain 中仅包含 window 对象。
- 执行函数 a 时,a 进入相应的执行环境(execution context)。
- 创建执行环境过程中,首先为 a 添加 scope 属性,即 a 的作用域,其值为第 1 步中的 scope chain。即 a.scope = a 的作用域链。
- 随后,执行环境创建活动对象(call object)。活动对象是具有属性的对象,无原型且不能通过 JavaScript 代码直接访问。创建完活动对象后,将其添加到 a 的作用域链最顶端。此时 a 的作用域链包含两个对象:a 的活动对象和 window 对象。
- 下一步是在活动对象上添加 arguments 属性,保存调用函数 a 时传递的参数。
- 最后,将函数 a 的所有形参和内部函数 b 的引用添加到 a 的活动对象上。此步完成函数 b 的定义,因此如同第 3 步,函数 b 的作用域链被设置为 b 被定义的环境,即 a 的作用域。
至此,函数 a 从定义到执行的步骤完成。a 返回函数 b 的引用给 c,又因函数 b 的作用域链包含对函数 a 活动对象的引用,即 b 可访问 a 中定义的所有变量和函数。函数 b 被 c 引用,b 又依赖 a,因此函数 a 返回后不会被 GC 回收。
当函数 b 执行时,亦会经历上述步骤。因此,执行时 b 的作用域链包含 3 个对象:b 的活动对象、a 的活动对象和 window 对象,如下图所示:

如图所示,当在函数 b 中访问变量时,搜索顺序是:
- 先搜索自身的活动对象,若存在则返回;若不存在,继续搜索函数 a 的活动对象,依次查找,直到找到为止。
- 若函数 b 存在 prototype 原型对象,则在查完自身活动对象后先查找自身的原型对象,再继续查找。这就是 JavaScript 中的变量查找机制。
- 若整个作用域链上都无法找到,则返回 undefined。
小结:本段提到两个重要词语:函数的定义与执行。文中指出函数的作用域在定义函数时已确定,而非在执行时确定(参考步骤 1 和 3)。用一段代码说明此问题:
function f(x) {
var g = function () { return x; }
return g;
}
var h = f(1);
alert(h());该代码中变量 h 指向了 f 中的匿名函数(由 g 返回)。
- 假设函数 h 的作用域在执行 alert(h()) 时确定,此时 h 的作用域链为:h 的活动对象 -> alert 的活动对象 -> window 对象。
- 假设函数 h 的作用域在定义时确定,即 h 指向的匿名函数在定义时已确定作用域。那么在执行时,h 的作用域链为:h 的活动对象 -> f 的活动对象 -> window 对象。
若第一种假设成立,输出值为 undefined;若第二种假设成立,输出值则为 1。
运行结果证明第二种假设正确,说明函数的作用域确实在定义该函数时已确定。
四、闭包的应用场景
- 保护函数内的变量安全。以最开始的例子为例,函数 a 中的 i 只有函数 b 能访问,无法通过其他途径访问,从而保护了 i 的安全性。
- 在内存中维持一个变量。如前例,由于闭包,函数 a 中的 i 一直存在于内存中,因此每次执行 c(),都会给 i 自加 1。
- 通过保护变量的安全实现 JS 私有属性和私有方法(不能被外部访问)推荐阅读:http://javascript.crockford.com/private.html
私有属性和方法在 Constructor 外是无法被访问的function Constructor(...) { var that = this; var membername = value; function membername(...) {...} }
上述三点为闭包最基础的应用场景,诸多经典案例皆由此衍生。
五、Javascript 的垃圾回收机制
在 JavaScript 中,若一个对象不再被引用,则该对象会被 GC 回收。若两个对象互相引用,而不再被第三者引用,则这两个互相引用的对象也会被回收。因为函数 a 被 b 引用,b 又被 a 外部的 c 引用,这就是为什么函数 a 执行后不会被回收的原因。
六、结语
理解 JavaScript 的闭包是迈向高级 JS 程序员的必经之路,理解其解释和运行机制才能写出更为安全和优雅的代码。如果您对本文有任何的建议和疑问,欢迎留言。转载请著名出处。
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
JavaScript新手常犯的编码错误与最佳实践指南
2026-11-04 22:12:38
