闭包是 JavaScript 中强大又容易踩坑的特性。它让函数能“记住”定义时的作用域,但这种记忆如果使用不当,就会导致两种典型问题:内存泄漏和变量共享错误。
内存泄漏:你以为丢了,其实它还活着
闭包让函数保留对外部变量的引用。只要闭包函数本身还被引用,它引用的外部变量就不会被垃圾回收。正常情况下这很合理——你需要那些变量,它们就该活着。但如果不小心让闭包“长生不老”,那些变量就永远无法释放。
经典场景:被遗忘的定时器或事件监听
function startTimer() {
const largeData = new Array(1000000).fill('*'); // 大量数据
const id = setInterval(() => {
// 闭包引用了 largeData
console.log(largeData.length);
}, 1000);
// 如果忘了 clearInterval(id),largeData 永远不会被回收
// 即使 startTimer 执行完毕,闭包依然持有引用
}
setInterval 的回调形成了一个闭包,它引用了 largeData。只要定时器没被清除,这个闭包就一直存在,它所引用的巨大数组就不会被释放。多次调用 startTimer 会不断分配内存而不释放,最终导致内存泄漏。
另一个常见场景:DOM 元素被移除后仍被闭包引用
function bindHandler() {
const element = document.getElementById('btn');
const heavyData = fetchSomeLargeData(); // 大型数据
element.addEventListener('click', function() {
// 这个闭包引用了 element 和 heavyData
console.log(heavyData);
});
// 某处代码移除了 element 节点
document.body.removeChild(element);
// 但事件监听器还在(可能现代浏览器会智能清理,但并非全部情况)
// 导致 element 和 heavyData 都无法被回收
}
解决方案
- 及时清除引用:不再需要闭包时,手动解除对变量的引用。例如定时器未清除时调用
clearInterval(id),事件监听用完后removeEventListener。 - 合理使用作用域:将大对象放在更小的作用域内,避免被长生命周期的闭包意外捕获。
- 工具排查:使用浏览器 DevTools 的 Memory 面板,拍取堆快照,查找“Detached”的 DOM 节点或意外存活的大对象,定位闭包引起的泄漏。
变量共享:同一个变量被多个闭包同时“记住”
闭包记住的是变量的引用,而不是创建闭包那一刻的变量值副本。当多个闭包共享同一个外部变量,而该变量在循环或异步操作中发生变化时,很容易出现“值不对”的问题。
经典问题:循环中创建闭包,用 var 声明循环变量
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i); // 期望输出 1 2 3,实际输出 4 4 4
}, 100);
}
var 声明的 i 没有块级作用域,整个循环共用一个 i。三个回调共享同一个变量的引用,当它们执行时,循环早已结束,i 已经变成 4。
用 let 解决
ES6 的 let 具有块级作用域,每次迭代都会创建一个新的 i 绑定,每个闭包捕获的是不同迭代的 i。
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i); // 正确输出 1 2 3
}, 100);
}
闭包在异步回调中共享状态引发的竞态问题
除了循环,多个异步回调共享同一个对象或变量也容易造成意料之外的覆盖。例如,连续发起多个请求,回调都操作同一个 result 变量,可能因为返回顺序不确定导致结果混乱。解决思路是用函数参数传递或使用独立的作用域(如 IIFE)为每个闭包创建独立副本。
如何避免变量共享错误
- 能用
let和const就尽量不用var,利用块级作用域天然隔离。 - 当需要捕获循环的当前值时,除了用
let,也可以用立即执行函数(IIFE)传入参数造成值拷贝。 - 在现代框架(如 React)中,状态或副作用若依赖外部变量,要清楚闭包捕获的是哪一次渲染的“快照”,必要时使用
useRef或函数式更新来获取最新值。
小结
闭包的内存泄漏往往源于“忘记切断引用”——定时器、事件监听器、持久的回调队列都是高发地带。变量共享问题则源于闭包捕获的是变量引用而非值的快照。理解这两点,配合现代语法的块级作用域和良好的清理习惯,就能充分享受闭包的强大,同时避开它的陷阱。