0%

javascript-interview-questions-02

var in for loop.

What’s the output of the following code?

1
2
3
4
5
6
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log(100);

Answer:

1
2
3
4
5
6
100
5
5
5
5
5

分析:

  1. 最后一行代码console.log(100);是同步代码,所以会先输出100。
  2. for循环一共执行5次,每次都会调用setTimeout,但是setTimeout是异步代码,JS引擎会将setTimeout的回调函数放到宏任务队列中,等待执行。
  3. 当前Event loop执行完毕后,会去执行宏任务队列中的任务,这时候setTimeout的回调函数才会被执行,但是这时候i已经变成了5,所以会输出5个5。

所以导致这段代码的问题有二,解决其中任何一个都可以让它输出0, 1, 2, 3, 4。

  1. 使用了var, var是函数作用域,所以所有的循环都共享一个i
  2. 使用了setTimeoutsetTimeout是整个循环结束后才开始执行的!如果我们把setTimeout删除就不会有问题了。以下代码works well!
    1
    2
    3
    for (var i = 0; i < 5; i++) {
    console.log(i);
    }

那么是不是不用setTimeout就没有问题了呢?不是的,我们来看这道题的一个变种:

1
2
3
4
5
6
7
8
9
10
const func = [];

for (var i = 0; i < 5; i++) {
func.push(() => {
console.log(i);
});
}

func.forEach((f) => f());
console.log(100);

这个变种没有使用setTimeout,但是结果依然是5个5。为什么呢?观察这个变种和原始题目我们可以看到,他们共同的特点都是在for循环结束后才执行代码,此时i的值已经是5了。这才是根本原因。

好了,现在我们来看看如何解决这个问题。

方法一,使用let代替var

由于let有块级作用域,所以每次循环都会创建一个新的变量i,而不是像var那样所有的循环都共享一个i。

1
2
3
4
5
6
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log(100);

方法二:将循环变量i参数化

说得再通俗一点,将setTimeout的逻辑单独抽离成一个函数print。这样每次循环的时候,都会将当前的i传递给print函数,这样setTimeout的代码加入宏任务队列时会记住当前的i值。

1
2
3
4
5
6
7
for (var i = 0; i < 5; i++) {
print(i);
}

function print(i) {
setTimeout(() => console.log(i), 1000);
}

如果觉得单独抽离函数不够优雅,可以使用IIFE(立即执行函数表达式)。注意,这里和闭包没有半毛钱关系,IIFE这里只是为了传递参数,也就是上面抽离函数的简化版。

1
2
3
4
5
6
7
8
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
});
})(i);
}
console.log(100);

方法三:使用setTimeout的第三个参数

这个方法可能很多同学都没有见过,其实setTimeout方法是有第三个参数的,这个参数是用来传递给回调函数的参数的。所以我们可以将当前的i值传递给setTimeout的回调函数。

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

为了做区分,我把回到函数中的i改成了j,这样就不会混淆了。循环变量i通过setTimeout的第三个参数传递给回调函数。

你学会了吗?