for(var i=0; i<5; i++){
setTimeout(function(){
console.log(i);
}, 1000);
}
// 输出:5 5 5 5 5(1秒后同时打印)具体发生了什么
这段代码涉及 JavaScript 的异步执行机制和闭包特性,导致输出结果与直觉不符。具体解释如下:
-
循环同步执行:
JavaScript 先同步执行 for 循环,由于 var 声明的变量 i 属于函数作用域(而非块级作用域),循环内的 setTimeout 虽然被依次调用,但其中的回调函数并未立即执行。
-
定时器异步执行:
setTimeout 的回调函数被放入任务队列,等待主线程执行完毕后才会触发。此时,for 循环早已结束,i 的值已经变为 5(循环条件 i<5 不满足时退出,此时 i=5)。
-
闭包捕获变量:
每个回调函数通过闭包捕获的是同一个变量 i(而非循环时的值)。当回调函数最终执行时,它们读取的 i 已固定为 5,因此全部打印 5。
所有 setTimeout 的延迟时间均为 1000ms,且是在循环中连续同步设置的。因此,它们会在循环结束约 1 秒后几乎同时从任务队列中被取出执行,导致 5 个 5 看起来是 “一起打印” 的。
要让输出符合预期(依次打印 0 1 2 3 4),需确保每个回调函数捕获的是当前循环迭代的值。以下是两种常见方法:
for(let i=0; i<5; i++){
setTimeout(function(){
console.log(i); // 正确输出:0 1 2 3 4(每秒一个)
}, 1000);
}
原理:let 声明的变量具有块级作用域,每次循环会创建一个新的 i 副本,闭包捕获的是当前迭代的 i。
for(var i=0; i<5; i++){
(function(j){
setTimeout(function(){
console.log(j); // 正确输出:0 1 2 3 4
}, 1000);
})(i); // 通过参数传递当前i的值
}
原理:利用立即执行函数创建独立闭包,将当前 i 的值传递给内部变量 j,避免所有回调共享同一个 i。
- 闭包捕获变量:闭包保存的是变量的引用,而非值的副本。
- 异步执行时机:
setTimeout 的回调会在循环结束后执行。
- 作用域差异:
var 是函数作用域,let 是块级作用域。