彻底搞懂 JavaScript 闭包:原理、陷阱与修复方案
JavaScript 闭包常让开发者感到困惑,但其本质是“有权访问另一个函数作用域中变量的函数”。本文从定义出发,深入剖析执行环境、作用域链与垃圾回收机制,揭示闭包维持数据的原理。同时,重点解决循环中变量捕获失败的经典缺陷,并提供通过立即执行函数(IIFE)修复的代码方案,助你写出更健壮的 JS 代码。
闭包的定义与核心特征
闭包是指有权访问另外一个函数作用域中的变量的函数。创建闭包的最常见方式,就是在一个函数内部创建另一个函数。
function a() {
var i = 0;
function b() {
alert(++i);
}
return b;
}
var c = a();
c(); // 弹出窗口显示 1
这段代码体现了闭包的两大特征:
1. 函数 b 嵌套在函数 a 内部;
2. 函数 a 返回了内部函数 b。
当执行 var c = a() 后,变量 c 指向了函数 b。由于外部变量 c 引用了内部函数 b,而 b 又依赖 a 中的变量 i,因此形成了一条引用链,这就是闭包。
为什么需要闭包?
闭包的主要作用在于保护变量和维持状态。
- 保护函数内的变量安全:如上例所示,变量
i只在函数a内部可见,外部无法直接访问,只能通过返回的函数b间接操作,从而避免了命名冲突和数据被意外修改。 - 使变量始终保存在内存中:一般函数执行完后,局部变量会被垃圾回收机制(GC)释放。但因为闭包的存在,函数
a返回后,其内部的i依然存在于内存中供b使用。这使得每次调用c()时,i都能保留上次的值并自增。
如果 a 不返回 b,则 a 和 b 互相引用但都不被外界引用,它们将被 GC 回收。只有当内部函数被外部引用时,闭包才真正建立,外层函数的资源才会被保留。
深入理解:执行环境与作用域链
要彻底理解闭包,需了解函数执行时的四个关键概念:执行环境、活动对象、作用域和作用域链。
- 定义时:当定义函数
a时,JS 解释器将a的作用域链设置为a所在的“环境”。若a是全局函数,作用域链仅包含window对象。 - 进入执行环境:当
a执行时,进入相应的执行环境。 - 创建作用域属性:为
a添加scope属性,指向定义时的作用域链。 - 创建活动对象:创建一个活动对象(Call Object),并将其添加到作用域链顶端。此时
a的作用域链包含a的活动对象和window。 - 初始化参数与内部函数:在活动对象上添加
arguments以及形参和内部函数b的引用。此时,b的作用域链被设置为a的作用域。
至此,a 完成定义到执行的步骤。返回 b 的引用给 c 后,由于 b 的作用域链包含对 a 活动对象的引用,b 可以访问 a 中的所有变量。因为 b 被 c 引用且依赖 a,所以 a 不会被回收。
变量查找机制:当在 b 中访问变量时,搜索顺序为:b 的活动对象 → a 的活动对象 → window。若存在原型对象,则在查找完自身活动对象后先查找原型对象。
内存管理与垃圾回收
在 JavaScript 中,若对象不再被引用,则被 GC 回收。闭包的代价是内存占用。
正常情况下,函数执行结束后,其执行上下文和作用域链占用的空间会被释放。
function test() {
var str = "hello world";
console.log(str);
}
test();
但在闭包场景下,由于内部函数引用了外部函数的变量,外部函数的作用域活动对象虽然会被销毁,但其内容被闭包函数的作用域链所引用,直到闭包函数被销毁后,这些内存才会被释放。滥用闭包会导致大量内存消耗,甚至引起内存泄漏。
经典缺陷:循环中的变量捕获
闭包最常见的副作用是:内部函数只能取到外层函数变量的最终值,而非定义时的快照值。
以下代码尝试通过循环绑定点击事件,期望弹出对应的索引值,但实际结果全是最终值(如数组长度为 4,则全部弹出 4):
/* 闭包缺陷演示 */
(function($) {
var result = new Array(),
i = 0;
for (; i < 10; i++) {
result[i] = function () {
return i;
};
}
$.RES1 = result;
})(jQuery);
// 执行数组中的函数,返回结果均为 10
$.RES1[0]();
第 1 步:分析错误原因
在 for 循环中,i 是外层匿名函数的局部变量。循环结束时,i 的值变为 10。数组中的每个函数都引用了这个同一个 i 地址。当任意一个函数被调用时,它读取的是当前 i 的值,即 10。
第 2 步:使用 IIFE 修复缺陷
通过引入立即执行函数表达式(IIFE),为每次循环创建一个独立的作用域,将当前的 i 值作为参数传递给内部函数,从而固定变量值。
/* 修复闭包缺陷 */
(function($) {
var result = new Array(),
i = 0;
for (; i < 10; i++) {
result[i] = function (num) {
return function () {
return num;
}
}(i); // 立即执行,传入当前 i 的值
}
$.RES2 = result;
})(jQuery);
// 调用闭包函数,正确返回对应索引
console.log($.RES2[0]()); // 输出 0
此修复方案在内存中创建了新的活动对象,使得 num 保存了每次循环时 i 的具体值,从而解决了引用最终值的问题。
实战案例:DOM 事件绑定
假设页面上有多个 <span> 元素,希望点击每个元素时弹出其索引值。
错误写法
直接使用 var 声明循环变量,所有点击事件共享同一个 i,导致点击任何元素都弹出最终的 i 值。
var spans = $("#divTest span");
for (var i = 0; i < spans.length; i++) {
spans[i].onclick = function () {
alert(i); // 始终弹出最终值
}
}
正确写法
使用 IIFE 包裹事件处理逻辑,将当前的 i 值传递给参数 num,确保每个事件监听器拥有独立的变量副本。
var spans2 = $("#divTest2 span");
$(document).ready(function () {
for (var i = 0; i < spans2.length; i++) {
(function (num) {
spans2[i].onclick = function () {
alert(num); // 弹出正确的索引值
}
})(i);
}
});
内部函数与逃脱机制
内部函数定义在另一个函数中,默认只能在外部函数内调用。若要在外部调用,必须通过以下方式“逃脱”:
- 赋值给全局变量:在外部函数中将内部函数赋值给全局变量。
- 作为返回值:外部函数直接返回内部函数的引用。
// 方式一:赋值给全局变量
var globalVar;
function outerFn() {
function innerFn() { /*...*/ }
globalVar = innerFn;
}
outerFn();
globalVar(); // 可调用
// innerFn(); // ReferenceError
这种逃脱能力意味着只要存在调用内部函数的可能,JS 运行时就必须保留相关内存,直到最后一个引用废弃,GC 才能回收。这也是闭包能够持久化数据的基础。
自由变量与多闭包交互
当内部函数引用了既不是自身局部变量也不是参数的变量时,该变量称为自由变量。外部函数的调用环境称为封闭闭包的环境。
如果一个外部函数返回多个内部函数,这些内部函数共享同一个自由变量实例:
function outerFn() {
var outerVar = 0;
function innerFn1() {
outerVar++;
document.write("Inner function 1 outerVar = " + outerVar + "<br/>");
}
function innerFn2() {
outerVar += 2;
document.write("Inner function 2 outerVar = " + outerVar + "<br/>");
}
return { fn1: innerFn1, fn2: innerFn2 };
}
var fnRef = outerFn();
fnRef.fn1(); // outerVar = 1
fnRef.fn2(); // outerVar = 3
fnRef.fn1(); // outerVar = 4
在此例中,innerFn1 和 innerFn2 共享 outerVar。重新调用 outerFn 会创建新的封闭环境和新的变量实例,互不影响。这体现了闭包在模拟面向对象私有变量方面的强大能力。
以上就是 JavaScript 闭包的详细内容,更多关于 JavaScript 高级特性与作用域的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













