详解for循环中使用setTimeout的问题

152 阅读2分钟

我正在参加「掘金·启航计划」。

下边这个经典面试题,乍一看,输出 0 1 2 3 4。其实不然,这里输出的是 5 个 5。

for(var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}

这个问题涉及到了 JavaScript 的作用域、变量提升与同步/异步。下面让我们详细解析一下这个问题。

面试题解析

不想看字的,可以直接看图,一图胜千言。

首先,确认下,这里for语句中用var声明的变量i不是for循环的局部变量,是跟for在相同的作用域中的变量(因为var没有块作用域,在块语句中形成不了块作用域)。

for循环是同步的,setTimeout是异步的。这里会生成5个setTimeout,每个setTimeoutcallback都会先放到异步队列中去,等到for循环同步执行完之后才会依次执行。

这里的i是全局作用域中的同一个ii在每次for循环的时候值都会加1,一直加到5判断不满足条件则终止for循环(此时的i=5)。同步任务执行完成,开始依次执行异步队列中setTimeout callback,打印5个5。

如果使用let声明变量iifor语句的局部变量,由于let可以在块语句中形成块级作用域,在循环的时候,每次的循环体都有自己的局部变量i。所以会依次输出0 1 2 3 4

for循环中()小括号部分是一个父作用域。{}块语句部分是一个子作用域,子作用域每次循环都会生成一个。

如何让其输出01234?

1、将var改为let

for(let i=0; i<5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}

2、使用闭包

使用IIFE将变量i作为参数传入,形成闭包,保证每次的变量i不会被污染。

for(var i=0; i<5; i++) {
  (function(j){
    setTimeout(function () {
      console.log(j);
    }, 1000);
  })(i)
}

3、使用setTimeout的第三个参数(这里其实也形成了闭包)。

for(let i=0; i<5; i++) {
  setTimeout(function (j) {
    console.log(j);
  }, 1000, i);
}

相关链接

for

并发模型与事件循环

Block块作用域

块语句

作用域

setTimeout