JS 函数的执行时机

270 阅读4分钟

JS任务执行

众所周知,JS是一门单线程语言。那就像我们去银行办理业务,而单线程意味着只有一个办理窗口,那么每个人都要等前一个人办理完成后,再去办理。同理JS也是一样,JS任务要一个一个按顺序执行。那么问题来了,如果前一个任务执行时间过长,后一个任务也要等着,这样必然增加了网页的加载时间。因此聪明的程序员将任务分成两类。

  • 同步任务:上一件事情没有完成,继续处理上一件事情,只有上一件事情完成了,才会做下一件事情 –> JS中大部分都是同步编程。
  • 异步任务:规划要做一件事情,但是不是当前立马去执行这件事情,需要等一定的时间,这样的话,我们不会等着他执行,而是继续执行下面的操作。

执行时机

函数执行的时机不同,运行结果也不同。下面我们按同步任务和异步任务两种情况,分别解释函数执行时机。

同步

  • 举例说明

    例1: 以下打印结果是什么?

    let a = 1 
    function fn() {console.log(a)}
    

    结果:什么都不会打印

    执行步骤:

    1. 声明变量a并赋值为1
    2. 声明函数fn
    3. 结束

    例2: 以下打印结果是什么?

    let a = 1 
    function fn() {console.log(a)}
    fn()
    

    结果:1

    执行步骤:

    1. 声明变量a并赋值为1
    2. 声明函数fn
    3. 执行fn() //打印出a
    4. 结束

    例3: 以下打印结果是什么?

    let a = 1 
    function fn() {console.log(a)}
    a = 2
    fn()
    

    结果:2

    执行步骤:

    1. 声明变量a并赋值为1
    2. 声明函数fn
    3. 将2赋值给a
    4. 执行fn() //打印出a
    5. 结束

    例4: 以下打印结果是什么?

    let a = 1 
    function fn() {console.log(a)}
    fn()
    a = 2
    

    结果:1

    执行步骤:

    1. 声明变量a并赋值为1
    2. 声明函数fn
    3. 执行fn() //打印出a
    4. 将2赋值给a
    5. 结束

通过上面几个例子可以看出,在同步任务中,确定函数运行的结果,需要关注函数执行的时间前的代码。因为后面的代码还没执行,不会影响函数的运行结果。

异步

  • 要说异步,就不得不用大名鼎鼎的setTimeout来举例子了

    例1: 以下打印结果是什么?

    let a = 1
    function fn(){
        setTimeout(()=>{console.log(a)},0)
    }
    fn() // 2
    a = 2  
    

    结果:2

    执行步骤:

    1. 声明变量a并赋值为1
    2. 声明函数fn
    3. 执行fn() –> setTimeout()会过一会执行 –>跳过setTimeout()
    4. 将2赋值为a
    5. 执行setTimeout() //打印出a
    6. 结束

例2 经典面试题: 以下打印结果是什么?
let a = 1 function fn(){ setTimeout(()=>{console.log(a)},0) } fn() // 2 a = 2
结果:正确答案是6个6 //666666 (可能的大部分人想的是 0,1,2,3,4,5)

我们需要重点理解**for循环执行步骤**:

1. i赋值为0
2. 判断i < 6 ?,满足进入第一循环
3. setTimeout()会过一会执行–>跳过setTimeout()继续执行
4. 执行i++,此时i的值为1
5. 判断i < 6 ?,满足进入第二循环
6. setTimeout()会过一会执行–>跳过setTimeout()继续执行
7. 执行i++,此时i的值为2
8. 省略…(重复执行前三个步骤)
9. 执行i++,此时i的值为6
10. 判断i < 6 ?,不满足跳出循环
11. 执行第一次循环的setTimeout() //打印出a
12. 执行第二次循环的setTimeout() //打印出a
13. 执行第三次循环的setTimeout() //打印出a
14. 执行第四次循环的setTimeout() //打印出a
15. 执行第五次循环的setTimeout() //打印出a
16. 执行第六次循环的setTimeout() //打印出a
17. 结束  

> 现在可以看出,由于setTimeout()的执行时间为for语句执行后,所以每次打印出的结果都为6  

setTimeout()的'过一会'执行究竟是多久呢?

上文中不止一次提到'过一会'了,那么'过一会'究竟是多久呢?

setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。也就是当同步任务的函数和语句执行完后,0秒或者立刻执行setTimeout(fn,0)。

那如果想打印 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循环生成的都是一个新的i, setTimeout里输出的i就是这个新的i,这个i是不会变化的,所以输出的就是正常的。

因为在for语句里用let声明变量是局部变量遵循块作用域,所以每次for循环执行时都会生成一个单独的作用域,也会生成一个新的i,相当于有6个 i。 此时,每次执行setTimeout()时都会打印出对应的i,打印结果就是0、1、2、3、4、5。

总结

函数执行的时机不同,运行结果也不同,关键是要弄清执行顺序和执行任务。