setTimeout 在循环中的执行时机

分析 var 声明变量在 setTimeout 循环中的输出结果

问题

以下代码执行后会输出什么?

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

解答

代码会输出 5 个 6。

执行过程:

  1. for 循环同步执行,创建 5 个 setTimeout 任务
  2. setTimeout 是宏任务,会被放入任务队列等待执行
  3. 循环结束时,i 的值已经变成 6
  4. 主线程空闲后,依次执行 5 个 setTimeout 回调
  5. 每个回调执行时,访问的都是同一个变量 i,此时 i 已经是 6

解决方案 1:使用 let

for(let i = 1; i <= 5; i++){
  setTimeout(function timer(){
    console.log(i) // 输出 1 2 3 4 5
  }, 0)
}

解决方案 2:使用闭包

for(var i = 1; i <= 5; i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j) // 输出 1 2 3 4 5
    }, 0)
  })(i)
}

关键点

  • setTimeout 是宏任务,在主线程同步代码执行完后才执行
  • var 声明的变量没有块级作用域,循环中的 i 是同一个变量
  • 回调函数执行时通过作用域链查找 i,此时循环已结束,i 为 6
  • 使用 let 可以创建块级作用域,每次循环都有独立的 i
  • 使用闭包可以保存每次循环时 i 的值