JS 函数的执行时机

193 阅读2分钟

这篇文章要探讨的问题有:

  1. 以下代码为什么会打印 6 个 6,而不是 0、1、2、3、4、5
  2. 以下代码如何改写,才会打印出 0、1、2、3、4、5

代码如下:

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

先看看没有 setTimeout 的情况

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

// 输出 0 1 2 3 4 5

以上代码不难理解,for 循环使得变量 i 从 0 往 6 遍历,且每次遍历都打印出当前 i 的值,即 0、1、2、3、4、5。当 i 为 6 时,不满足条件,循环结束。

可如果在循环体中加了 setTimeout,情况就不同了:

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

// 输出 6 6 6 6 6 6

以上代码,i 仍然从 0 往 6 遍历,但由于使用了 setTimeout,导致 console.log 不是立即执行,而是“尽快执行”。是在 for 循环执行完毕之后,再“尽快执行” console.log 函数。由于 for 循环执行完毕后 i 的值已经变成 6,且遍历过程中有 6 个 console.log 还未执行,所以最终 console.log 执行了 6 遍,打印出 6 个 6 。

如何在使用 setTimeout 的同时,又要输出 0、1、2、3、4、5 ?

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

// 输出 0 1 2 3 4 5

以上代码,将 let 声明变量放到 for 循环中,换言之,将 let 与 for 一起使用,JS 就会在每次遍历时,多创建一个 i,每次遍历时 i 都是不同的,所以最终输出了不同的结果,而不是 6 个 6 。

如果不用 let,而用 var,则可以使用立即执行函数和闭包来解决此问题

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

// 输出 0 1 2 3 4 5

以上代码,每次遍历时都调用一个匿名函数,并且将当前的 i 传入匿名函数,再“尽快”打印出 i。因为每次遍历都调用一个匿名函数,等 for 循环执行完,就调用了 6 个匿名函数,这 6 个匿名函数都有各自的作用域,且给每个匿名函数传入的参数都是不同的,所以最后输出了 6 个不同的值。