JavaScript函数的执行时机

143 阅读4分钟

由于JS是单线程的,同一时间内它只能做一件事情,多个任务执行就需要排队。

那在使用JS的时候可不可以在做一件事情的同时去处理另一件事呢?

是可以的。我们可以通过异步代码(比如:setTimeout)去执行新的任务。

那是否是使用了异步代码就可以在同步任务(主线程上执行的代码)进行的同时一起并行执行呢?

并不可以。因为JS的执行机制是这样的:

  • 先执行主线程上的同步任务;
  • 异步任务放入任务队列中;
  • 一旦主线程上的所有同步任务都执行完后,系统就会依次读取任务队列中的异步任务,于是被读取的异步任务结束等待状态,系统将异步任务放入主线程中开始执行,执行完毕后继续去任务队列中读取下一个任务,直到任务队列中的任务全部执行完毕,代码结束。像这种主线程不断重复获取任务、执行任务、再获取、再执行的过程,被称为事件循环。

可以通过这段代码来感受一下这个过程:(使用chrome控制台操作)

function fn() {
    let lastTime = new Date()
    let  newTime = new Date()
while(newTime - lastTime < 5000){
    newTime = new Date()
}
    console.log('已经过了五秒了!')
}
# 控制台执行
fn()
setTimeout(() => {console.log('异步代码已执行!')},1000)
fn()

上面代码的执行结果顺序是这样的,第一个fn()执行,五秒后输出信息,第二个fn()执行,五秒后输出信息,再过1秒后输出异步代码已执行!。结果就是11秒后才输出异步代码中的信息,通过这个例子可以清晰的了解到,异步代码总要等到同步代码执行完后才会被执行,这就是JS的执行机制。

了解完JS的执行机制,我们再来看看作用域对代码执行的影响。

问题来了,下面代码的执行结果是什么?

// 代码1
let i = 0
for(i = 0; i < 6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}
// 代码2
for(let i = 0; i < 6; i++){
    setTimeout(() => {
        console.log(i)
    },0)
}

代码1结果:6个6

代码2结果:0,1,2,3,4,5

解读:代码1中的i是一个全局变量,作用域是全局。整个for除了setTimeout都属同步代码,他们会在执行栈中直接执行完毕,执行完后再开始执行setTimeout中的函数,而此时i的值已经变为6了,所以for循环中创建的6个异步代码都输出同一个i的值。

那为什么代码2中只是将let放入for中声明并定义i就能让输出结果为0,1,2,3,4,5呢?因为JS会在for和let一起使用的时候会多加东西,会在每次循环开始的时候创建一个看起来一样的i。换一种理解方式就是,通过let为每次的循环内的代码创建了一个独立的作用域,这个作用域中的i就是每次循环i的值,而setTimeout执行的时候根据作用域规则中的「就近原则」会去寻找和自己同一个作用域的变量i,于是最终的输出结果就是0,1,2,3,4,5了。

由于作用域的原因,我们可以实现如代码2中的变量和异步代码输出同步结果的情况。除了像代码2中的let使用方式还有哪些方式可以实现同样的效果呢?tips:通过作用域下手。

  1. 使用立即执行函数运行代码,因为函数可以创建一个独立的作用域。有两种方式可以操作
// 方式1:立即执行函数在setTimeout的第一个参数上。
for(var i = 0; i < 6; i++) {
    setTimeout((function(){
    console.log(i)
  })(),0)
}
// 这种方式和上一种是一样的方法,但是打印i时的作用域是不相同的,这里的i要传入立即执行函数中,
// 为了方便理解,用变量a来区分。
// 上面的是打印的全局变量i
// 另外就是方式1的方法,它们都并没有进入异步函数中执行,是在进入之前就随着同步代码执行完毕了。
// 所以这样虽然顺序打印了0,1,2,3,4但是是一种比较鸡贼的办法。
for(var i = 0; i < 6; i++) {
    setTimeout((function(a){
    console.log(a)
  })(i),0)
}
// 方式2:立即执行函数包装了setTimeout函数
for(var i = 0; i < 6; i++){
    (function(a){
    setTimeout(function(){
    console.log(a)
},0)
})(i)

  1. 在setTimeout前用一个新的变量去接收循环,属于多此一举。
for(var i = 0; i < 6; i++){
    let j = i
  setTimeout(function(){
    console.log(j)
  },0)
}
  1. 利用最新浏览器支持的setTimeout函数将参数3传给function。传多个时,function可以使用多个变量去接收。
for(var i = 0; i < 5; i++) {
  setTimeout(function(a){
    console.log(a)
  },0,i)
}

以上就是关于JavaScript的执行机制以及作用域的相关知识介绍!如有错误,欢迎指正!