
昨天去一家公司面试,原本信心满满 一切进展得很顺利。直到面试官抛出一个问题:“能不能用闭包实现一个简单的计数器?”
听起来是个很基础的考题,我胸有成竹地写出了以下代码:
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 输出: 1
console.log(counter()); // 输出: 2面试官点了点头,说:“这没问题,那如果加上异步操作呢?比如,延迟打印计数器的值会发生什么?”
我稍微一愣,连忙补充代码:
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(`计数器值是: ${i}`);
}, i * 1000);
}结果输出却全是 “计数器值是: 4”。面试官笑着问:“你知道为什么吗?”这一刻,我意识到闭包并没有我想象得那么简单。
闭包是指 函数可以记住它定义时的词法作用域,即使这个函数是在词法作用域之外执行的。简单说,闭包让你能访问函数外部的变量。
闭包有两个关键点:
回到我的代码,问题出在 for 循环和 setTimeout 的结合上。
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(`计数器值是: ${i}`);
}, i * 1000);
}setTimeout 中的回调函数形成了闭包,但它捕获的是变量 i 的 引用。由于 var 定义的变量是 函数作用域,所以 for 循环结束时,i 的最终值是 4。所有的回调函数都访问同一个 i,因此输出一致。
我在面试中赶紧调整代码,通过 IIFE 将每次循环的 i 传递进去,并锁定值:
for (var i = 1; i <= 3; i++) {
(function (currentI) {
setTimeout(function () {
console.log(`计数器值是: ${currentI}`);
}, currentI * 1000);
})(i);
}这样每次 currentI 的值都是独立的,最终输出:
计数器值是: 1
计数器值是: 2
计数器值是: 3let 替代 var幸好面试官没有让我死磕 IIFE,他提醒说 ES6 的块级作用域更优雅。于是我改用 let:
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(`计数器值是: ${i}`);
}, i * 1000);
}let 会为每次循环创建一个新的块级作用域,天然避免了闭包的引用问题。
闭包的问题不仅仅是记住它的定义,而是要理解它在各种场景下的表现。比如,异步操作、变量共享等问题,往往会暴露我们对闭包的理解深度。
面试结束后,我在闭包上深挖了一下才发现,掌握它不仅对写代码有帮助,也是衡量一个开发者功力的关键指标。闭包虽然是 js 的“老生常谈”,但真正理解透彻的人却不多。