JS 函数的执行时机

308 阅读3分钟

一句话理解

  • 函数定义和函数执行是顺序关系。
    function sayHi() {
        console.log('hi~')
    }
    sayHi

问:以上代码的执行结果是什么? 答:什么都没有,因为函数只是定义了,并没有执行。

    function sayHi() {
        console.log('hi~')
    }
    sayHi();

以上才是调用。

    let sayHi = function() { console.log('hi~') }
    let fn = sayHi
    fn();

以上代码的执行结果和 sayHi() 是一样的。fnsayHi 都只是匿名函数的引用而已。

函数的执行时机不同,执行结果就不同

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

以上代码的打印结果为 1,代码从上往下顺序执行:声明变量 a 并赋值,声明函数 fn (当然函数提升会将函数定义放到最上面),调用函数 fn

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

    a = 2
    fn()

以上代码的打印结果为 2,执行顺序:声明变量 a 并赋值,声明函数 fn (并没有调用)(当然函数提升会将函数定义放到最上面),先改变了 a 变量的值,然后调用了函数 fn,所以打印的结果为改变后的 a 的值,如果函数的调用在 a 变量的值改变之前,那么打印的结果就为改变前的值。

下面来看一个特殊情况:

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

    fn()
    a = 2

在函数体中定义了一个计时器,时间为 0 秒后执行并输出 a 的值。按照以上理论,打印的结果应该是 1,但实际上打印的结果是 2。

因为在 JS 中,所有的回调函数,都为异步代码。JS 代码在执行时,会优先顺序执行执行栈中的同步代码。而异步代码则会由异步处理进程放入异步队列排队,当执行栈中的同步代码全部执行完毕后,再回头来执行异步代码。而以上代码的 log 函数恰恰放在了定时器的回调函数中。

我们来搜索一下定时器的用法:

setTimeout.png 定时器可以接收一个函数作为参数,也可以接收一个字符串。 那么我们将以上代码修改一下:

    let a = 1

    function fn() {
        setTimeout(console.log(a), 0)
    }

    fn()
    a = 2

此时,打印的结果就为 1

我们再来看一个变态的情况:

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

打印的结果:6 个 6。

我们来看执行顺序:首先,定义变量 i 并赋值,然后进入循环。判断初始值,符合,进入循环体,发现定时器,先拿去排队。继续循环,一直到 i 的值等于 6。这时,不符合循环条件,退出循环,此时,i 的值为 6。然后再执行异步代码,也就是定时器里的回调函数,打印 i 的值:6 个 6,为什么是 6 个? 因为每循环一次就创建一个定时器,但定时器里的回调函数不执行。

for 循环的循环条件中,使用全局作用域下用 let 声明的 i 和用 var 声明的 i 的效果是一样的。

那除了在循环条件内用 let 声明 i 以外,还有没有别的方法打印出 0 ~ 5 呢?

有的,比如: 还记得定时器的语法吗?它支持参数传入一个字符串,这样就没有异步代码了。

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

另一个方法:

闭包

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