JS 函数的执行时机

195 阅读2分钟

分析下面代码

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

代码描述

首先在外部作用域中定义了一个let i = 0 然后通过for循环创建了六个定时器,那么当定时器触发时console.log会打印出什么呢。

执行原理

首先定时器是异步执行(异步代码加载完之后会等到线程空闲再执行)的并不是你所看到的再在for循环中每次循环一次就执行一次,这里定时器设置为0表示没有任何等待的时间,只要到JS引擎控制的线程没有代码段在运行的时候就立即执行。

那么通过for循环创建的六个定时器会在JS引擎吧手头的任务(for循环)执行完成再执行定时器。for循环结束后i===6接着六个定时器执行时打印出来的值也分别都是6。

就如同下面代码

function fn1(){
    console.log(a)
}
let a = 0
fn1() //0
a = 1
fn1() //1

fn1打印出来的值是被外部的a变量所控制的,换句话说定时器中i的值也是被外部变量i所控制的。这也就解释了为什么会打印出六个6.

解决办法

需要做的就是将每次循环的值固定在定时器中,这里大致就可以分为两种方法

  1. 局部变量
  2. 通过函数参数

1. 直接使用局部变量 let

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

下面两种基本上是第一种的变体,不过更加容易理解

1.2 使用let局部变量保存

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

1.3 使用const局部变量保存

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

每次循环都会得到一个局部变量j,也就是说每次循环的j和之前的j都是不同的,对应的setTimeout执行函数中保存的j都是不同的。

2 使用闭包+立即执行函数并入传参

通过参数固定定时器中i的值

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

2.1 优化一下

外部的立即执行函数看起来多余了,简化一下就有了下面代码,通过立即执行函数并传入参数将值固定在定时器中。

let j = 0;
for (; j < 6; j++) {
    !function (aaa) {
        setTimeout(() => {
            console.log(aaa)
        }, 0)
    }(j) //传参
}

这里使用aaa证明外部的变量j和内部(定时器所在的函数环境)的值是没有关系的,只不过每次循环都赋值了一份j的值到定时器中。

2.2setTimeout传参

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