JavaScript学习笔记 - 函数的执行时机

214 阅读2分钟

JavaScript 的函数在调用时存在一个调用时机的问题,在不同的时机调用会得到不同的结果,本文将举例说明调用时机对结果的影响。

  • 【例】
let a = 1
function fn(){
    console.log(a)
}
fn()

显然,结果会打印出1。

  • 【变体1】
let a = 1
function fn(){
    console.log(a)
}
a = 2
fn()

也很显然,结果打印出2。

  • 【变体2】
let a = 1
function fn(){
    console.log(a)
}
fn()
a = 2

调用函数 fn 后,再将 a 赋值为2,因此当然会打印出1。

以上例子都是显而易见的,它就是我们主观感受上的结果,但是下面的例子可能就会有些反直觉了。

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

结果打印出2。setTimeout 会过一段时间再执行,在调用 fn 时,间隔一段时间再打印 a 的值,因此会先执行a = 2,接着再打印 a 的值。

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

结果不是打印0、1、2、3、4、5,而是打印出6个6。理由同上,在每次执行循环体时,都会经过一段时间再打印 i 的值,JS 引擎会一直等待。等到循环结束后,i 的值为6,此时执行6次console.log(i),因此打印出6个6。

这个结果会非常的反直觉,但是将代码做微小调整,就能得到符合直觉预期的结果。

  • 【变体】
for(let i = 0; i < 6; i++){
    setTimeout(()=>{
        console.log(i)
    }, 0)
}

仅将 let i = 0 移到 for 循环的圆括号中,就会打印出0、1、2、3、4、5了。这是 ES 6 新增的特性,for 和 let 结合使用时,每次循环都会创建一个新的 i,就好像每次都把 i 记录了下来。这样即使是过一段时间再执行6次console.log(i),每次都是不同的 i ,因此每次打印的值自然不同。

原因分析

事实上,第一个例子之所以会打印6个6,是作用域的问题。这里面的 i 在 for 循环的外面声明,其作用域是全局,这样当循环结束后,执行console.log(i)时,找到的 i 就是全局的 i,此时 i 的值为6,因此只能打印6个6。

要解决这个问题,就要改变 i 的作用域。

变体中,let i的作用域为 for 循环代码块,所以每一次循环中的 i 都存在于独立的作用域,即每次循环的 i 是不同的,于是每次就打出了不同的值。

除此之外,还有其他的方法也可以达到同样的效果,如:

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

每次循环都会调用 fn,将 i 的值传入并且保留在 fn 的作用域中,由于每次传入 fn 中的 i 值不同,所以每次打印出的结果自然也就不同了。