闭包是JavaScript中内部函数访问外部变量并逃逸执行时自动形成的运行时行为;需同时满足函数嵌套、变量捕获、逃逸执行三条件;用于封装私有状态、解决循环索引问题,但可能引发内存泄漏。
闭包 是 JavaScript 中函数“记住自己出生地”
怎么一眼识别闭包?看这三步
闭包不是写出来的,是“跑出来”的。判断一个函数是不是闭包,只看三点:
- 函数嵌套:内部函数定义在外部函数体内
- 变量捕获:
inner函数体里直接用了outer的局部变量(比如let count、const config) - 逃逸执行:这个
inner被返回、赋值给变量、传给setTimeout或事件监听器,**脱离了原始调用环境**
缺一不可。比如下面这个就不是闭包:
function outer() {
const x = 1;
function inner() {
console.log('I ignore x'); // 没引用外部变量
}
return inner;
}
inner 虽然嵌套,但没捕获任何自由变量,引擎不会为它保留作用域链。
最常用的实际用途:封装私有状态
JavaScript 没有 #private 字段(ES2025+ 才有,且不兼容旧环境),而闭包是长期稳定、全版本兼容的私有变量方案。
典型写法:
function createCounter() {
let count = 0; // 外部变量 → 被闭包保护
return {
increment() { count++; },
getCount() { return count; }
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
这里 count 对外完全不可见,连 counter.count 都是 undefined。所有修改必须走暴露的方法。
- 避免全局污染:变量锁死在函数作用域内
- 防止误改:外部无法绕过逻辑直接赋值
- 适合配置模块、单例缓存、状态管理器等场景
循环中绑定正确索引:别再用 var 了
这是新手踩坑最多的地方。下面这段代码会输出 5 个 5:
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
因为 var 是函数作用域,i 全局共享;所有回调共用同一个 i,等真正执行时循环早已结束,i === 5。
用闭包修复(ES5 方式):
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
更现代、更推荐的写法(ES6+):
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
let 声明每次迭代都创建新绑定,本质就是语言层帮你做了闭包封装——你不用手动写 IIFE,但原理一样。
容易被忽略的代价:内存泄漏风险
闭包让变量“活”得更久——只要内部函数还存在,它引用的所有外部变量就无法被垃圾回收。
- 如果闭包持有 DOM 节点、大数组、或整个对象树,而你又忘了清理(比如没移除事件监听器),内存就悄悄涨上去了
- 在长生命周期对象(如页面级模块)中频繁创建闭包,尤其要检查是否无意保留了对大对象的引用
- 调试时可借助 Chrome DevTools 的 Memory > Heap Snapshot,筛选
Closure类型,看哪些变量被意外滞留
闭包本身不是问题,问题在于“该释放时不释放”。它像一把没有保险栓的刀——好用,但得清楚自己握着哪一头。








