【五】JS执行

前言
本篇章是偏理解性的博客,主要讲述在js环境中变量、方法执行方式。理解执行顺序,能够更好地帮助你在开发中解决奇奇怪怪的问题。
面试回答
1.执行上下文:执行上下文可以简单理解成一个对象,这个对象包含变量对象、作用域链、this指向,一般就全局执行上下文和函数执行上下文。
2.变量提升:变量提升就是在赋值操作之前,就使用对应的变量,导致变量变成undefined。原因在于执行过程中,首先会建立活动对象,然后构建作用域链,再确定this指向,最后才是代码执行。创建变量或者函数的步骤都在建立活动对象阶段,而赋值操作是在代码执行阶段,所以才会找不到。
3.this:this永远指向函数运行时所在的对象,而不是函数被创建时所在的对象。改变this指向通常有三种方法,bind、call、apply,bind会返回一个新函数 ,call在改变this指向后还执行了函数,且能够接收多个参数,而apply与call的区别在于apply接收数组作为传入参数。
4.手写apply:首先判断传入的参数是否为值类型,如果是值类型,则直接返回该值类型,如果是引用类型,则给该参数添加fn属性用来保存当前this,这个this指向当前的调用函数。下一步判断是否存在其他参数,如果有就将它展开,并将它作为参数传入到上面的this函数中,并把执行结果保存到result里,然后删除fn属性,并返回result。至于call与apply的区别在于传入的参数不一样,bind与apply的区别在于bind返回一个新函数并不执行。
5.事件循环(Event Loop):事件循环是浏览器的一种解决JS单线程运行时不阻塞的机制,具体流程是这样的:1、首先所有同步任务都在主线程上执行,形成一个执行栈。2、如果遇到了异步任务,就丢到主线程外的任务队列,等异步任务有结果后,就会转移到调用栈中。3、再然后执行栈中所有同步任务执行完毕,就会读取调用栈,如果有任务就丢到执行栈,开始执行这个任务中同步任务。4、最后主线程不断重复上面的几个步骤,这就是事件循环的一个机制。从执行顺序上来看,就是一个主线程 > 微任务 > 宏任务,有结果 > 宏任务,无结果的顺序。
知识点
javascript函数执行过程主要由创建执行环境、进入函数调用栈、执行、销毁这四个阶段构成,下面我们来一一理解每一个阶段所做的事情。
1.创建执行环境
执行环境,也就是执行上下文,分为全局环境、函数环境、Eval 函数执行环境。
全局环境指的是JS默认的代码执行环境,是最外围的一个执行环境,在web浏览器中,全局执行环境被认为是window对象。一旦代码被载入,引擎最先进入的是这个环境,全局环境不会被自动回收,只有在关闭浏览器窗口的时候才会被销毁,所以在定义全局变量一定要格外小心。
函数环境是一个相对于全局环境的概念,由于在执行代码时,线程就是在全局环境和函数环境之间来回穿梭的,可以简单理解为函数环境即任何一个函数被调用都会创建一个新的执行环境,执行结束后返回全局环境,而创建的函数环境等待垃圾回收。
Eval 函数执行环境不经常用,尽量避免,这里不做讨论。
2.函数调用栈
在创建执行环境后,函数/代码下一个阶段会被放入一个栈中,这个栈被称为函数调用栈。js根据函数的调用(执行)来决定执行顺序。函数调用栈的栈底永远都是全局环境,而栈顶就是当前正在执行函数的环境。当栈顶的执行环境执行完之后,就会出栈,并把执行权交给之前的执行环境。举例:
1.首先 A() ;A 函数执行了,A执行环境入栈。
2.A函数执行时,又调用了 B(),B又执行了,B入栈。
3.B中没有可执行的函数了,B执行完出栈。
4.继续执行A, A中没有可执行的函数了,A执行完 出栈。

上述例子只是为了阐明函数调用栈的作用,具体的执行过程,包括执行上下文、作用域链、this指向等操作是在执行过程发生的。
3.执行过程
当函数被调用时,会创建一个新的函数执行环境,该创建过程主要由两个阶段组成:建立阶段 、代码执行阶段
A. 建立阶段(发生在调用/执行一个函数时,但是在执行函数内部的具体代码之前)
1.建立活动对象
2.构建作用域链
3.确定this的指向
B. 代码执行阶段
1.执行函数内部的具体代码
接下来,我们逐个理解其中的步骤:
A.1. 建立活动对象
这里我们首先理解两个概念:变量对象(Variable object,VO) 、活动对象(Activation object)
变量对象(Variable object,VO) 是一个与执行上下文相关的特殊对象,在函数上下文中,VO是不能直接访问的。变量对象用来存储上下文的函数声明、 函数形参、变量声明。优先级:函数声明>函数的形参>变量。
函数声明:每找到一个函数声明,就在活动对象下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用,如果上述函数名已经存在于活动对象下,那么则会被新的函数引用所覆盖。
函数形参:建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值 。没有实参的话,属性值为undefined。
变量声明:每找到一个变量声明,就在活动对象下面用变量名建立一个属性,该属性值为undefined。如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
活动对象(Activation object,AO):由于变量对象不能访问,在函数执行阶段,由变量对象转化而来的可访问对象。
举例:
解析:我这边的理解跟参考资料有所不同,望指正。
块级作用域,简单来说就是函数内部和{}之间的部分。变量提升也是在建立阶段产生的问题,即在赋值操作之前(赋值操作在代码执行阶段),就使用对应的变量,从而使变量为undefined,举例:
参考资料:https://article.itxueyuan.com/O0mA6
A.2.构建作用域链
作用域链的最前端,始终都是当前执行的代码所在函数的活动对象。下一个活动对象(AO)为包含本函数的外部函数的AO,以此类推。最末端,为全局环境的变量对象。
注意:
1.虽然作用域链是在函数调用时构建的,但是它跟调用顺序(进入调用栈的顺序)无关,因为它只跟包含关系(函数、包含函数的嵌套关系)有关。
2.作用域链是创建函数的时候就创建了,此时的链只有全局变量对象,保存在函数的[[Scope]]属性中,然后函数执行时的,只是通过复制该属性中的对象来构建作用域链。
举例:
b函数被a函数包含,a函数被window全局环境包含。
参考资料:https://blog.csdn.net/weixin_33919950/article/details/89625339
A.3.确定this的指向
this的指向在函数定义的时候是确定不了的,只有函数被调用的时候才能确定,并且this的最终指向的是那个调用它的对象 。
情况1:匿名函数
匿名函数this的默认指向为windows
情况2:函数调用
1.如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window。
2.如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。
3.如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象,也就是说this指向的是直接调用它的对象。
情况3:构造函数中的this指向
首先new关键字会创建一个空的对象,然后会自动调用一个函数apply方法,将this指向这个空对象,这样的话函数内部的this就会被这个空的对象替代,可以参考后续关于new操作符的知识点。
当构造函数的this碰到return时:如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。
情况4:箭头函数的this指向
箭头函数的this指向,是指向箭头函数被创建时外部作用域(要么是window,要么是最近一层的局部函数)的this指向的对象,而不是调用时指定this指向。举例:
情况5:call、bind、apply的this指向
由于js中this的指向受函数运行环境、调用的影响,指向经常改变,使得开发变得困难和模糊,所以在写一些复杂函数的时候经常会用到this指向绑定,以避免出现不必要的问题,call、apply、bind基本都能实现这一功能。
1.bind:bind用于将函数体内的this绑定到某个对象,然后返回一个新函数
2.call:call方法可以指定this 的指向,然后再指定的作用域中,执行函数。call可以接受多个参数,第一个参数是this指向的对象,之后的是函数回调所需的入参
3.apply:apply 和call作用类似,也是改变this指向,然后调用该函数,唯一区别是apply接收数组作为函数执行时的参数
PS:call、bind、apply能实现以下基础功能,
间接调用函数,改变this,劫持其他对象的方法
两个函数实现继承
为类数组(arguments和nodeList)添加数组方法,如push、pop
(function(){
Array.prototype.push.call(arguments,'王五');
console.log(arguments);//['张三','李四','王五']
})('张三','李四')合并数组、求数组内最大值
4.面试题:手动实现bind、call、apply
实现call(obj,arg,arg....)1.改变this的指向。2.传入参数。3.返回函数执行结果
实现bind代码:
B.1.执行函数内部的具体代码
执行代码阶段最主要有两个事情:变量赋值、JS事件机制。其中变量赋值比较好理解,这里不做过多解释,接下来着重理解JS事件机制。
JavaScript是单线程指的是同一时间只能干一件事情,只有前面的事情执行完,才能执行后面的事情。导致遇到耗时的任务时后面的代码无法执行,因此有了同步、异步任务。
同步任务:for循环、new Promise等除了异步任务外的其他任务
异步任务:微任务、宏任务,其中微任务有Promise.then、process.nextTick、queueMicrotask,宏任务有setTimeout、setInterval、IO、UI渲染等
async/await只是一个语法糖,只是帮助我们返回一个Promise而已,如果方法被调用的话,async相当于new Promise ,await后面的代码相当于.then里的代码,且.then代码必须得在promise中resolve后才会执行,否则.then代码不会执行,await在async代码执行完毕后,进入微任务队列。
了解任务的概念后,现在整理一下任务执行的流程:
1、所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
2、主线程之外,还存在一个任务队列(task queue)。只要是异步任务,就丢到这个任务队列,有了运行结果,就在任务队列之中放置一个事件。
3、当任务队列中的异步任务有了结果之后,就会将任务移动到调用栈中。
4、一旦执行栈中的所有同步任务执行完毕,系统就会读取调用栈,开始执行该异步任务中同步任务。
5、主线程不断重复上面的第三步,称为事件循环(Event Loop)。
总结执行顺序:主线程(同步任务,new Promise)> Promise.then(微任务)> setTimeout(宏任务,有结果) > setTimeout(宏任务,无结果),同类型任务按照先进先出的顺序执行。理清流程可得:

面试题:
4.销毁执行环境和活动对象
某个执行环境所有代码执行完之后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁,这边闭包有所不同,将会在下篇博客中,进一步理解。全局执行环境只会在关了浏览器或者程序的时候才被销毁。
最后
走过路过,不要错过,点赞、收藏、评论三连~