var
in for loop.
What’s the output of the following code?
1 | for (var i = 0; i < 5; i++) { |
Answer:
1 | 100 |
分析:
- 最后一行代码
console.log(100);
是同步代码,所以会先输出100。 - for循环一共执行5次,每次都会调用
setTimeout
,但是setTimeout
是异步代码,JS引擎会将setTimeout的回调函数放到宏任务队列中,等待执行。 - 当前Event loop执行完毕后,会去执行宏任务队列中的任务,这时候
setTimeout
的回调函数才会被执行,但是这时候i
已经变成了5,所以会输出5个5。
所以导致这段代码的问题有二,解决其中任何一个都可以让它输出0, 1, 2, 3, 4。
- 使用了
var
,var
是函数作用域,所以所有的循环都共享一个i
。 - 使用了
setTimeout
。setTimeout
是整个循环结束后才开始执行的!如果我们把setTimeout
删除就不会有问题了。以下代码works well!1
2
3for (var i = 0; i < 5; i++) {
console.log(i);
}
那么是不是不用setTimeout
就没有问题了呢?不是的,我们来看这道题的一个变种:
1 | const func = []; |
这个变种没有使用setTimeout
,但是结果依然是5个5。为什么呢?观察这个变种和原始题目我们可以看到,他们共同的特点都是在for
循环结束后才执行代码,此时i的值已经是5了。这才是根本原因。
好了,现在我们来看看如何解决这个问题。
方法一,使用let
代替var
。
由于let
有块级作用域,所以每次循环都会创建一个新的变量i,而不是像var
那样所有的循环都共享一个i。
1 | for (let i = 0; i < 5; i++) { |
方法二:将循环变量i参数化
说得再通俗一点,将setTimeout的逻辑单独抽离成一个函数print
。这样每次循环的时候,都会将当前的i传递给print
函数,这样setTimeout
的代码加入宏任务队列时会记住当前的i值。
1 | for (var i = 0; i < 5; i++) { |
如果觉得单独抽离函数不够优雅,可以使用IIFE(立即执行函数表达式)。注意,这里和闭包没有半毛钱关系,IIFE这里只是为了传递参数,也就是上面抽离函数的简化版。
1 | for (var i = 0; i < 5; i++) { |
方法三:使用setTimeout
的第三个参数
这个方法可能很多同学都没有见过,其实setTimeout
方法是有第三个参数的,这个参数是用来传递给回调函数的参数的。所以我们可以将当前的i值传递给setTimeout
的回调函数。
1 | for (var i = 0; i < 5; i++) { |
为了做区分,我把回到函数中的i
改成了j
,这样就不会混淆了。循环变量i
通过setTimeout
的第三个参数传递给回调函数。
你学会了吗?