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中的变量作用域和闭包的概念。在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
都是独立的。
原理: 因为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的效果。
相关参考: