web学习笔记编程技术学习与分享
首页 / 前端开发 / 正文

闭包不只是「函数里的函数」

2026 年 08 月 22 日约 3 分钟阅读 前端开发

很多教程把闭包解释成「函数内部返回一个函数」,这个描述只说了形式,没说清机制。闭包的本质是:函数在定义时就记住了它所在的词法作用域,即使这个作用域已经执行结束。

从一段计数器看起

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 是语言层面的替代方案,但理解闭包仍然是读懂老代码的前提。

一句话总结:闭包不是特性,而是词法作用域加函数作为一等公民的自然结果。

JavaScript作用域