闭包是JavaScript函数作用域与执行上下文共同作用的自然结果,核心在于函数静态绑定定义时的词法环境([[Environment]]),并捕获外层变量引用而非值快照。

闭包不是语法糖,也不是高级技巧——它是 JavaScript 函数作用域与执行上下文共同作用的自然结果。只要函数在定义时能访问其外层作用域的变量,并且这个函数在定义作用域之外被调用,闭包就已形成。
闭包的核心机制:函数记住了它被创建时的词法环境
JavaScript 中的每个函数在创建时都会绑定一个 [[Environment]] 内部属性,指向其定义时所在的作用域链。这个绑定是静态的、不可更改的,和函数在哪被调用无关。
常见错误现象:for (var i = 0; i console.log(i), 100); } 输出三个 3,不是因为闭包“失效”,而是因为 var 声明的 i 是函数作用域共享的,所有回调都闭包引用了同一个 i 变量。
修复方式(任选其一):
立即学习“Java免费学习笔记(深入)”;
- 改用
let:它为每次循环迭代创建独立绑定,每个回调闭包各自的i - 显式创建闭包:用立即执行函数包裹
i,如(function(i) { setTimeout(() => console.log(i), 100); })(i) - 用
setTimeout的第三个参数传参:setTimeout(console.log, 100, i)
模块模式与私有状态封装
闭包是实现“私有变量”的唯一原生手段(ES6 # 私有字段是语法层面补充,底层仍依赖闭包语义)。典型场景是避免全局污染、隐藏实现细节。
示例:计数器模块
function createCounter() {
let count = 0;
return {
increment() { count++; },
getValue() { return count; }
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getValue()); // 1
// count 变量无法从外部直接访问
注意点:
- 每次调用
createCounter()都生成**独立的闭包环境**,多个实例互不干扰 -
count存在于堆内存中,只要返回的对象还存活,该变量就不会被 GC 回收 - 滥用会导致内存泄漏——比如给 DOM 元素绑定事件后未解绑,而事件处理器又闭包了大量数据
事件处理与异步回调中的状态捕获
真实项目里,闭包最常出现在事件监听、fetch 回调、定时器等异步场景中,用于“记住当时的状态”。
使用场景举例:
- 列表项点击需携带索引或 ID:
items.forEach((item, i) => btn.addEventListener('click', () => handleClick(item, i))) - 防抖函数内部需要保存上一次的
timeoutId:function debounce(fn, delay) { let timeoutId; return function() { clearTimeout(timeoutId); timeoutId = setTimeout(fn, delay); }; } - Promise 链中传递中间值:
fetch(url).then(res => res.json()).then(data => process(data, extraConfig)),其中extraConfig就靠闭包带入
性能影响:闭包本身开销极小,但若闭包引用了大对象(如整个 DOM 树、大型数组),且该函数长期驻留(如全局事件处理器),就会延长这些对象的生命周期,间接增加内存压力。
调试闭包:Chrome DevTools 中怎么看它在哪儿
在断点处打开 “Scope” 面板,能看到 “Closure” 条目,展开即可查看当前函数闭包捕获的所有变量及其值。这是验证闭包是否按预期工作最直接的方式。
容易被忽略的地方:
- 闭包捕获的是**变量的引用,不是值快照**——如果外层变量后续被修改,闭包内读到的就是新值
- 箭头函数没有自己的
this和arguments,但它依然会形成闭包,捕获外层的this和arguments - 不要试图用闭包“冻结”对象——它只保证变量可访问,不阻止对象属性被修改










