JavaScript 5 setTimeout()的执行时机

113 阅读2分钟

2022.8.2

在 JS 中有一个奇特的setTimeout(),将它放在下面的函数中,能让结果变得很有趣:

let i = 0
for(i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}

不了解setTimeout()的人,可能会认为上述函数的运行结果是 0、1、2、3、4、5,实际上它会打印出 6 个 6。

1.为什么上面的代码会打印出 6 个 6?

因为setTimeout()的逻辑是,执行完当前的所有代码后,立即执行setTimeout()里的代码。

而在文章开头的代码中,所谓“当前的所有代码”,就包含了整个 for 循环。即当 for 循环结束后setTimeout()里的代码才会被执行。

在 for 循环结束后,setTimeout()执行前,内存中有些什么呢?由于 for 循环执行了 6 次(i 为 0~5 总共 6 次),此时积累了 6 个等待执行的setTimeout(),同时还有值为 6 的 i。

终于轮到 6 个setTimeout()执行,它们把此时值为 6 的 i ,依次用console.log(i)打印了出来,于是,我们就看到了 6 个 6。

2.有什么方法,可以配合 setTimeout() 打印出 0、1、2、3、4、5?

第一种方法是 for let 配合:

for(let i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}

第二种方法,是利用立即执行函数的局部变量 j,来单独接受每个循环里 i 的值,然后令setTimeout()依次打印出 j。

let i = 0
for(i = 0; i!=6; i++){
  !function(){
    let j = i
    setTimeout(()=>{
      console.log(j)
    },0)
  }()
}

当然,我还无法理解原理,为什么立即执行函数跟内置let i = 0的效果一样?它们之间有什么关系?可惜,我现在没有能力深究。

第三种方法略麻烦,是将i++的流程移动到setTimeout()里面,但是需要在 for 循环结束后将 i 重置为 0:

let i = 0
for(i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i++)
  },0)
}
i = 0