JS 函数的执行时机

128 阅读3分钟

误区

  • setTimeout() 如果延迟时间为0,应该立即执行。
  • 在 for 循环中使用var 和 let 声明变量区别很大

setTimeout()

事实上以 0 为第二参数调用 setTimeout 并不表示在 0 毫秒后就立即调用回调函数,而是会被放到事件队列中去,然后等待浏览器空闲之后执行。因此,setTimeout 中的代码并没有立刻执行,而是等到整个 for 循环结束之后才执行的。

以下三个场景

  • 场景1
for(var i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)          //输出6个6
  },0)
}
  1. 执行 for 循环,每一次 for 循环的时候,setTimeout 都执行一次,但是里面的代码没有被执行,而是被放到了任务队列里面,等待执行。
  2. 变量 i 是 var 声明的,在全局范围内都有效,所以全局只有一个变量i,每一次循环,变量i的值都会发生改变。
  3. setTimeout 里面的代码 ()=>{console.log(i)}中的i指向的就是全局的i。即每次执行setTimeout 里面的代码运行时输出的是最后一轮的i的值6,输出6个6
  • 场景2
for(let i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)          //输出0 1 2 3 4 5 
  },0)
}
  1. 在 for 循环中使用 let 声明变量,循环体会有一个隐藏的作用域
  2. 变量 i 是 let 声明的,每一次循环变量i都会重新声明,即当前的 i 只在本轮循环有效,所以每一次循环的 i 其实都是一个新的变量
  3. 隐藏作用域存放了每次循环产生的i
  4. 每一次循环的变量i是不同的,值也不同,因此执行 setTimeout 里面的代码会有6个不一样的数
  • 场景3
let i = 0
for(i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)          //输出6个6
  },0)
}

在 for 循环外使用 let 定义变量,情况和在for循环中使用 var 声明变量一样

可以打印出 0、1、2、3、4、5的其他方法

  • 使用const声明变量
for(var i = 0; i<6; i++){
    const x = i
    setTimeout(()=>{
      console.log(x)
    })
}

这种方法与场景1类似,场景2有一个隐藏的作用域来存放变量i,而这种方式直接写明了将每一次循环后的 i 存放由 const 声明的x,并且每一次for循环变量 x 会重新声明一次,值也会不同,因此执行 setTimeout 里面的代码会有6个不一样的数

  • 闭包
for(var i = 0; i<6; i++){
  !function(j){
      setTimeout(()=>{
        console.log(j)
      },0)
  }(i)
}

采用匿名函数实现专门用来存储变量i,这个函数对象有一个本地私有变量 j ,i 是全局变量其值随外部改变,但是本地的私有变量 j 不会受影响,即为每次迭代都准备一个新的被闭包的作用域。在每次迭代时对 i 进行拷贝,闭包一个新的作用域,这些作用域中的每一个都拥有一个变量给 setTimeout 函数使用。

总结

  • 结果出现连续的6个6的问题所在:
    由于作用域的工作方式,6个setTimeout函数闭包在同一个共享的全局作用域上,所有函数共享一个指向相同的 i 的引用,因此出现6个6。
  • 如何解决
    在循环期间,都“捕捉”一份对 i 的拷贝即每次循环都需要一个 块作用域 ,使用const声明变量这种方法也是创造了一个块并且每一次循环时这个块中重新声明一个变量,就完成了拷贝功能。