探讨关于JS函数的执行时机的问题~

JS函数的执行时机和函数被调用的时机有关,函数被调用时才会被执行,调用时机不同,函数的执行结果也不同。

先通过几个例子来理解这句话:

例 1:

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

结果:a不会被打印,因为没有调用函数,函数未被执行。

例 2:

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

结果:a被打印为1。

例 3:

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

a = 2
fn()
// 2

结果:a被打印为2, 可以通过函数声明的位置确认,函数里的变量a是离函数最近的let声明的变量a,但在函数被调用前a被赋值为2。

例 4:

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

fn()
a = 2
// 1

结果:a被打印为1, 可以通过函数声明的位置确认,函数里的变量a是离函数最近的let声明的变量a,并且在函数调用前a的值未被改变。

例 5:

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

fn()
a = 2
// 2

结果:a被打印为2

原因:console.log(a)是异步执行的,实际上的执行时间是a=2赋值执行之后

先来看看setTimeout()函数的原理

💁🏼‍♀️插一嘴:异步

Javascript语言的执行环境是"单线程"。也就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。为了解决阻塞问题,Javascript语言将任务的执行模式分成两种:同步和异步。setTimeout函数就可以让任务异步执行。

var timerId = setTimeout(func|code, delay)
  • setTimeout函数接受两个参数,第一个参数func|code是将要推迟执行的函数名或者一段代码,第二个参数delay是推迟执行的毫秒

    数。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。

  • 计时器到达时间点,计时器里事件处理程序或回调函数都不会立即运行,而是立即排队,一旦线程有空闲就执行。

  • 如果这个时间设为 0,就代表立即插入队列,但不是立即执行,仍然要等待前面代码执行完毕,只有在 JS线程中没有任何同步代码要执行的前提下才会执行异步代码。

例 6:

let i = 0
for(i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}
//不是 0、1、2、3、4、5
//而是 6 个 6

setTimeout函数让console.log(i)变为异步执行,每次循环都会把console.log(i)加入队列,但要等待for循环执行完毕后(i的值变为6),才开始执行队列中的6个console.log(i)语句,因为i定义在全局作用域中,6个console.log(i)语句共享一个i的引用,所以打印为6个6。

让上面代码打印 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

和例6相比,使用let在for循环语句的圆括号之内声明赋值,在圆括号之间会有一个隐藏的作用域,并且在每次执行循环体之前,JS 引擎会把 i 在循环体的上下文中重新声明及初始化一次。

上面代码相当于

for(let i = 0; i<6; i++){
  let i = 圆括号隐藏作用域中的i 
  setTimeout(()=>{
    console.log(i)
  },0)
}

这样的话,在for循环中,每次循环都会产生一个闭包作用域,i会被声明5次,产生5个不同的i,console.log(i)语句中的i不再是例6中for循环外的全局i,而是for循环语句内每次都重新声明赋值的i,第一次循环的i被声明赋值为0,加入队列的语句为 console.log(0),第二次……所以结果是0、1、2、3、4、5

方法二

参考博客:JS 函数的执行时机

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

在for循环体内使用立即执行函数时,都每次循环都会创建一个新的作用域,使得setTimeout函数的回调可以将新的作用域封闭在每个循环内部,每个循环中都会有一个有正确值的变量i。