JS 函数的执行时机

211 阅读4分钟

Hello大家好!我是Cathy海希。如果提到JS的函数你会最先想到什么知识点呢?

闭包?this?箭头函数?调用栈?函数提升?

是啊,函数的相关知识点太多了,也都非常重要。今天我想来跟大家聊其中的一点:函数的执行时机。

执行时机很重要

代码1

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

问:打印出来多少?

答:不知道,因为函数根本就没有被调用啊。

代码2

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

问:打印出来多少?

答:2。

是不是开始有点疑惑了呢?🤔

要彻底理解这段代码,我们还得先弄清楚setTimeout这个API的运行机制。

setTimeout

根据MDN文档的解释,

setTimeout()方法设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码。

我们再看一次代码2。

fn()的意思就是等0秒之后,执行里面的函数。

那问题又出来了,什么的0秒之后?是执行完fn()0秒之后吗?

正确答案是,执行完a = 2的0秒之后。

在这里,我为大家推荐一个可以可视化JS的call stack/Web Apis/Callback Queue的网站,latentflip.com,我利用这里的画面来为大家解释setTimeout()里面的函数究竟是何时执行的。

  1. 因为a=2这样的表达式在这个网站里无法显现出来,所以我把它改成另外一个带setTimeout的函数fn2;
  2. 把定时器的时间改成1000毫秒(1秒)。

这样子大家看得清楚一点。代码如下:

function fn() {
    setTimeout(function A() {
        console.log('a')
    }, 1000)
}
fn()
function fn2() {
    setTimeout(function B() {
        console.log('b')  
    }, 1000)
}
fn2()

注:此网站不支持箭头函数语法。

从这个GIF我们可以发现一个令人吃惊的现象:定时器到期之后,A()并没有立马被执行,而且排队进行了Call Queue,而是fn2()先被执行了!!

所以我在此大胆地判断一下:MDN写的

该定时器在定时器到期后执行一个函数或指定的一段代码。

其实并没有把后半部分的情况说完整,结合我们的实际情况就是,如果fn()后面还有其它代码的话,先执行它们,然后才会最后处理乖乖排队中的set Timeout里面的函数。

回到代码2,我们也就清楚代码执行的顺序了。

执行fn()

⬇️

️setTimeout里的函数在0秒之后,进入Callback Queue排队

⬇️

执行a = 2

⬇️

所有代码都执行完了,再执行正在Callback Queue排队的函数

善解人意还是迷惑行为?

上述代码的执行顺序可能对于刚入门的初学者来说比较难理解,JS就做出了一个决定:既然你们都觉得很难理解,那我就出一个容易符合你们思路的写法吧!

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

大家可以在控制台试试,这样子打印出来的结果就成了0、1、2、3、4、5。因为JS在forlet一起使用的时候加了东西:在每次循环的时候,多创建一个i。(不过会让人觉得:那这里使用setTimeout还有意义吗?😓)

延伸

那有没有其它办法同样可以得到0、1、2、3、4、5呢?

我想起了昨天学习的【在不产生全局变量全局函数的情况下,生成局部变量】的知识点。

如果能够在for循环里获得局部变量,也就是每一次循环的i,不是就可以得到上述打印效果了吗?让我们来试试看。

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

for循环里定义一个匿名函数,传入形参x,然后再最前面添加!,然后用实参i立即调用执行这个匿名函数。这样子我们就总可以获得局部作用域里面的i值了。

总结

JS函数是不是看上去越来越难但是也越来越有意思了呢?我更加期待结下来的学习了!那我们就很快再见啦~

See you next time👋