
![]()
for(var i = 0; i < 5; i++) {
setTimeout(function(){
console.log(i)
}, 0)
}
答案:输出结果为5个5。
解释:异步代码需要等同步代码先执行,所以当异步定时器执行时, 同步的for循环已经循环完毕
因为setTimeout 是异步执行的,所以必须等主流程执行完了才会执行,等同于
for(var i = 0; i < 5; i++) {
}
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
// 先循环,i变成了5,然后再打印5次i ————这里只是假想,便于理解
这段JavaScript代码的执行过程如下:
- 首先创建一个for循环,初始化变量i=0
- 每次循环时,都会调用setTimeout函数,将回调函数放入事件队列
- 由于setTimeout的延迟时间为0,回调函数会在当前执行栈清空后立即执行
- 循环会快速执行5次(i从0到4),每次都会注册一个setTimeout回调
- 当主线程代码执行完毕后,事件循环开始处理队列中的回调函数
- 此时i的值已经是5(因为循环已经结束)
- 所以5个回调函数执行时,都会打印出5
关键点:
- JavaScript是单线程的,setTimeout的回调会在当前代码执行完后才执行
- 由于var没有块级作用域,所有回调函数共享同一个i变量
- 当回调执行时,i已经是循环结束后的值5

这个问题的核心在于理解JavaScript中的变量作用域和闭包的概念。在JavaScript中,for循环中的变量i在循环结束后才被赋值,因此所有的setTimeout回调函数都共享同一个变量i,即循环结束后的值5。这是因为setTimeout是异步操作,当循环结束后,变量i的值已经变为5,此时所有的回调函数引用的是同一个变量i的最后一个值,即5。因此,无论何时这些回调函数被执行,它们都将输出5。
现在来思考一个问题,假如说我就是想用这种方式来实现输出0 1 2 3 4呢?
为了解决这个问题,有几种方法可以确保每个迭代输出正确的值:
1、使用let关键字:在ES6中引入的let关键字可以确保每个迭代中的变量i有一个独立的作用域,这样每个回调函数就可以捕获到迭代过程中的正确值。例如:
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 0);
}
// 因为let关键字劫持了for循环的块作用域,产生了类似闭包的效果。并且在for循环中使用let来定义循环变量还会有一个特殊效果:每一次循环都会重新声明变量i,随后的每个循环都会使用上一个循环结束时的值来初始化这个变量i。
这样,每个回调函数都会输出0到4的值,因为每个迭代中的i都是独立的。
这段代码的执行过程可以分为以下几个关键步骤:
1、变量声明阶段:
- 使用
let声明循环变量i,具有块级作用域特性同步执行阶段: - 主线程立即执行for循环,连续创建5个定时器任务
- 每次循环时都会创建一个新的块级作用域,保存当前
i值
2、异步执行阶段:
- 当主线程执行栈清空后(即使延迟为0),事件循环开始处理宏任务队列
- 依次执行5个定时器回调函数,此时每个回调都能访问到对应的块级作用域中的
i值
3、输出结果:
- 最终控制台会按顺序打印:0、1、2、3、4
- 关键点在于
let创建的块级作用域为每次迭代都保留了独立的i值
与var声明的区别:
- 若使用
var,由于变量提升和函数级作用域,会输出5个5 let的块级作用域特性确保了每个定时器回调都能捕获到对应的迭代值
执行时序特点:
- 虽然定时器延迟设为0,但回调仍要等待同步代码执行完毕
- 实际延迟时间取决于主线程执行耗时(本例中极短)
原理: 因为for循环头部的let不仅将i绑定到for循环快中,事实上它将其重新绑定到循环体的每一次迭代中,确保上一次迭代结束的值重新被赋值。setTimeout里面的function()属于一个新的域,通过 var 定义的变量是无法传入到这个函数执行域中的,通过使用 let 来声明块变量,这时候变量就能作用于这个块,所以 function就能使用 i 这个变量了;这个匿名函数的参数作用域 和 for参数的作用域 不一样,是利用了这一点来完成的。这个匿名函数的作用域有点类似类的属性,是可以被内层方法使用的。
2、使用立即执行函数表达式(IIFE):通过创建一个立即执行函数表达式来立即执行一个函数,并将当前迭代的值作为参数传递给该函数,可以确保每个回调函数捕获到正确的值。例如:
for (var i = 0; i < 5; i++) {
(function(j) { // 这个匿名函数生成了闭包的效果,新建了一个作用域,这个作用域接收到每次循环的i值保存了下来,即使循环结束,闭包形成的作用域也不会被销毁
setTimeout(function() {
console.log(j);
}, 0);
})(i);
}
// 这是因为for循环里定义的i变量其实暴露在全局作用域内,于是5个定时器里的匿名函数它们其实共享了同一个作用域里的同一个变量。
// 所以如果想要0,1,2,3,4的结果,就要在每次循环的时候,把当前的i值单独存下来,怎么存下当前的循环i值??
// 利用闭包的原理,闭包使一个函数可以继续访问它定义时的作用域。而这个新生成的作用域将每一次循环的当前i值单独保存了下来。
或
for (var i = 0; i < 5; i++) {
(() => {
var privateI = i;
setTimeout(() => {
console.log(privateI);
}, 0);
})()
}
这样,每个回调函数都会输出0到4的值,因为立即执行函数表达式创建了一个新的作用域,捕获了当前迭代的值i。
3、使用setTimeout的第三个参数:setTimeout函数的第三个参数可以传递一个额外的参数给回调函数。通过使用这个参数,可以确保每个回调函数都接收到正确的值。例如:
for (var i = 0; i < 5; i++) {
setTimeout(function(num) {
return function() {
console.log(num);
};
}(i), 0);
}
这种方法虽然看起来复杂一些,但它的原理与立即执行函数表达式类似,都是为了确保每个迭代中的变量有一个独立的作用域。不过上面的代码片段有误,正确的使用方式应该是将内部函数作为参数传递给setTimeout:
for (var i = 0; i < 5; i++) {
setTimeout(function(j) { return function() { console.log(j); }; }(i), 0);
}
但这种方法在实际应用中较少使用,因为它增加了代码的复杂性。更推荐使用let关键字或立即执行函数表达式来解决问题。
4、使用Promise
for(var i=0;i<5;i++){
Promise.resolve(i).then(i=>{
setTimeout(()=>{
console.log(i);
}, 0);
})
}
5、使用try catch
let可以实现块作用域的效果,但是它是ES6语法,在低版本语法的时候如何生成块作用域?答案是:使用try...catch语句。
for (var i = 0; i < 5; i++) {
try {
throw i
} catch (i) {
setTimeout(() => {
console.log(i);
}, 0);
}
}
因为try...catch语句的catch后面的花括号是一个块作用域,和let的效果一样。所以在try语句块里抛出循环变量i,然后在catch的块作用域里接收到传过来的i,就可以将循环变量保存下来,实现类似闭包和let的效果。
相关参考:
参考:
16万+

被折叠的 条评论
为什么被折叠?



