跳到主要内容

如何解决下面的循环输出问题?

面试速答(30 秒版 TL;DR)

  • 经典题:for (var i...) setTimeout(...) 最终输出都是同一个值(通常是循环结束后的 i)。
  • 原因:var函数作用域,循环里共享同一个 i 绑定;定时器回调是异步,执行时循环早结束。
  • 最佳解:用 let(块级作用域)为每次迭代创建新的绑定;或用 IIFE/函数参数把值“传进去”形成闭包隔离。

题目复现

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 常见输出:3 3 3

为什么会这样?

  • var i 只有一个,循环每次只是把同一个 i 改掉。
  • setTimeout 回调会在本轮同步代码跑完后才执行。
  • 回调执行时,i 已经是 3 了,所以每次都打印 3

解法 1(推荐):用 let 让每次迭代有独立绑定

for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 0 1 2

面试补充点:letfor 循环中会为每次迭代创建新的词法环境绑定,因此回调闭包各自捕获不同的 i


解法 2:IIFE 把当前值作为参数传入(闭包隔离)

for (var i = 0; i < 3; i++) {
((j) => {
setTimeout(() => console.log(j), 0);
})(i);
}
// 0 1 2

这里回调闭包用的是 j(每次 IIFE 调用都会生成新的 j 绑定)。


解法 3:利用 setTimeout 的参数(把值“传给回调”)

for (var i = 0; i < 3; i++) {
setTimeout(
(j) => {
console.log(j);
},
0,
i
);
}
// 0 1 2

这个方案不依赖闭包隔离 i,而是让 setTimeout 在触发回调时把参数带进去。


典型追问:如果我要每秒输出一次 0、1、2 呢?

for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), i * 1000);
}

易错点/坑

  • 只会背“用 let”但说不清原因:要能讲清 var 作用域、异步时机、以及 let for 的迭代环境。
  • 试图用“复制变量值”解释闭包:更严谨的说法是“捕获的是绑定/环境引用”,let 让绑定变成每次迭代独立。

速记要点(可背诵)

  • var + 异步回调:共享一个 i,回调执行时 i 已变成最终值。
  • 解法:let 或 IIFE 参数,核心是让每次迭代有独立的绑定。