在 JavaScript 里闭包的一些陷阱
时间:2026-3-30 23:53 作者:独元殇 分类: 前端技术
作为一个前端程序员,我每天的任务大部分时间都是在和 js 斗智斗勇。虽然有了 AI 的辅助,但是懂点常见的坑,还是能节省大把的时间和 词元 token 的。js 太美了:
今天说的这些陷阱主要是闭包里的。
闭包就是一直反常识的,在函数内部创建的变量,在执行完函数后居然还能贮存到内存里的一种情况。很容易造成内存泄漏。
比如这个例子:
function createCounter() {
let count = 0; // 这个变量被“封闭”在内部
return function() {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 输出: 1
counter(); // 输出: 2
counter(); // 输出: 3
你会发现,函数内部的 count 阴魂不散,每次执行都会加一。
这就是一个典型的闭包。
循环里面的闭包
大家在控制台执行下面的代码试试:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 解决方案 IIFE 作用域
for (var i = 0; i < 5; i++) {
(function(k) {
setTimeout(function() {
console.log(k);
}, 1000);
})(i);
}
它会一口气输出 5 个 5!
为什么,很简单,当 setTimeout 执行时,循环已经结束。
循环是一瞬间就跑完了 5 次,这个时候 5 个 setTimeout 都时间距离确实很近,但不至于都是同一个值,因此值这个时候没定死,而是拿的 i 的引用。它们 1s 后执行的时候,去访问的是 i ,而不是 1 2 3 4 5 ,循环早已结束了,i 肯定是 5 了!所以输出了 5 个 5 。
这是因为 var 的变量,能在函数作用域外提升。
解决方案有两种,要么你使用 let ,这个更符合人的思维,不会出现这些你很难预料的 bug 。要么就加一个 (function(){})(i); 包起来,这叫 IIFE (立即调用函数表达式)。它每次创建都会新建一个函数作用域,然后将我们的 i 拍一个快照,传递给 k 。
至于 var 和 let,可以这样理解,var 的 5 个 i ,其实是一个共用的 i ,而 let 的 五个 i ,是 5 个独立的 i 。互不干扰。
this 绑定
看看这个:
const obj = {
name: 'oliver',
report: function() {
return function() {
console.log(`I am ${this.name}`);
};
}
};
obj.report()(); // I am undefined
// 解决方案 1
// 输出时 bind 原来那个 obj
obj.report().bind(obj)(); // I am oliver
// 解决方案 2
// 使用更符合人逻辑直觉的 ()=>{} 箭头函数
const obj = {
name: 'oliver',
report: function() {
return () => {
console.log(`I am ${this.name}`);
};
}
};
理论上,this 貌似应该指向 name ,但却输出了 undefined 。原因很简单,return 的那个函数,有自己的生态,它有自己的「新 this」,覆盖了以前那个 this。
解决方案 1 很鸡肋。看起来不美观。还得上 ES6 大法!你意味 箭头函数 只是写着简单了一些?不不不,它也是让一些东西变得更加的直观符合逻辑。箭头函数是可以捕获上文的 this 的。
普通函数,和 箭头函数 还是不一样的。普通函数,this 默认指向 window 的,而箭头函数,压根没 自己的 this ,都是借用的外层的 this 。
内存泄漏
和我们最初那个例子差不多,这个更直观:
function object() {
const largeObj = new Array(1000000).fill('*');
return function() {
console.log('hi');
};
}
object(); // 这样执行,不会造成内存泄漏,因为 V8 引擎会优化
const leak = object(); // 这种就会永久贮存内存了!!!
let leak2 = object(); // 这种也会
// 解决方案
leak2 = null; // 注意,const 的 leak 不行,必须是 let 的 leak2
首先,如果 leak 和 leak2 是在作用域内,那么这个作用域销毁时,会自动销毁 largeObj。
但是如果 leak 和 leak2 ,是在全局变量,那... 如果你不 null 一下主动清除,就会一直保持在内存里。