闭包不只是「函数里的函数」
很多教程把闭包解释成「函数内部返回一个函数」,这个描述只说了形式,没说清机制。闭包的本质是:函数在定义时就记住了它所在的词法作用域,即使这个作用域已经执行结束。
从一段计数器看起
function counter() {
let n = 0;
return () => ++n;
}
const c = counter();
c(); // 1
c(); // 2
counter 执行完后,它的执行上下文本应销毁,但因为返回的箭头函数引用了 n,这个变量被保留在内存里。这个「保留的作用域」就是闭包。
循环里的经典问题
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3 3 3
}
var 是函数作用域,三个回调共享同一个 i。改成 let 后,每次迭代都会创建新的绑定,输出 0 1 2。这不是语法糖的魔法,而是规范为 let 定义了「每轮迭代复制一份」的语义。
如果必须用 var,就用 IIFE 显式捕获:
for (var i = 0; i < 3; i++) {
(function (j) { setTimeout(() => console.log(j), 0); })(i);
}
闭包与内存
闭包不会自动释放被引用的变量。常见泄漏场景:
- 事件监听器里引用了大对象,元素移除后监听未解绑;
- 定时器持有外部作用域,
clearInterval忘记调用; - 缓存对象没有上限,只增不减。
排查时可以在 DevTools 的 Memory 面板拍快照,看某个构造函数实例的 retained size。不要凭直觉猜。
闭包的正面用途
模块模式就是闭包的直接应用:用函数作用域模拟私有变量。
const store = (() => {
let state = {};
return {
get: k => state[k],
set: (k, v) => { state[k] = v; }
};
})();
外部无法直接访问 state,只能通过暴露的方法读写。ES Module 的 import/export 是语言层面的替代方案,但理解闭包仍然是读懂老代码的前提。
一句话总结:闭包不是特性,而是词法作用域加函数作为一等公民的自然结果。