js高级-js代码如何运行

251 阅读10分钟
html文档中我们经常会通过在<script></stript>标签中添加我们的js代码,html文档在解析道script标签时,js引擎
就会解析我们的js代码,从而达到动态页面的效果,在这个过程中,js引擎会帮助我们做很多事情,来保证我们的代码能够正常运行,
这里面涉及到ECS(执行上下文栈),GEC(全局执行上下文),VO,GO(全局对象),AO,FEC,环境变量,环境记录,作用域提升,
作用域链等等这些概念,理解这些概念对我们编写js 代码有很大的帮助,特在此做梳理总结。

一:js代码的运行环境(原生js)

    我们都知道使用node可以运行js文件,使用浏览器也可以运行我们编写的js文件,具体的原因是,在浏览器中和node 环境中
都嵌入了js引擎,是js 引擎为我们的js 代码的运行提供了保证。从普通使用者角度来说,不必细究这里的细节,但是作为一个前端
开发人员来说,当我们对js引擎所做的事情有更深的了解之后,就能更加容易的利用引擎的特性,来编写更高质量的js代码,毕竟了解
真相,才能获取真正的自由。
    我个人认为js的代码运行环境,就是js引擎为我们创造的运行环境,在这里总结几个这个环境中的概念,以及这些概念与我们的
代码有什么关系做备忘录,辅助理解。

ECS(函数调用栈)&& GEC(全局执行上下文)- 作用域提升 && FEC(函数执行上下文)

1.执行上下文栈(调用栈)ECS

    js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。ECS 首先是一个栈,这个栈的作用就是执行放入到栈中的上下文,比如执行全局执行上下文GEC,执行函数执行上下文FEC。

2.GEC(全局执行上下文)- 作用域提升

    全局的代码块为了执行代码会构建一个 Global Execution Context(GEC)。
        GEC被放入到ECS中里面包含两部分内容:
        (1)第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是
        并不会赋值;这个过程也称之为变量的作用域提升(hoisting)
        (2)第二部分:在代码执行中,对变量赋值,或者执行其他的函数;
    

3.FEC(函数执行上下文):

       js引擎在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,
   简称FEC),并且压入到EC Stack中。
       FEC中包含三部分内容: 
           第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO)
           第二部分:作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找; 
           第三部分:this绑定的值:这个我会在后续的文章中详细解析;
   

截屏2022-04-23 下午7.52.03.png

VO && GO && AO

1.VO

       早期ECMA规范这么写到:  每一个执行上下文会被关联到一个变量环境中(variable object,VO),在源代码中的变量
和函数声明会被作为属性添加到VO中。对于函数来说,函数也会被添加到VO中。VO是一个变量,在编译过程中会根据执行的内容做对应
赋值,在ECS中放入函数执行时时VO=AO,在ECS中放入GEC时,VO=GO。

2.GO

    js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO),该对象在所有的作用域(scope)都可以访问; 
    GO里面包含两部分内容:
        (1)里面会包含DateArrayStringNumbersetTimeoutsetInterval等等; 
        (2)其中还有一个window属性指向自己;

3.AO

    在解析函数成为AST树结构时,会创建一个Activation Object(AO):
        AO中包含形参、arguments、函数定义和指向函数对象、定义的变量;

环境变量(VE) && 环境记录(VR)

    上面阐述的VO,GO,AO 其实是ECMA5之前或者ECMA5早期的概念,ECMA5后期及之后,ECMA修改了这几个概念,但大致意思
    相差不多。
    环境变量(VE)& 环境记录(VR)
        每一个执行上下文会关联到一个变量环境中(Variable Environment)中,在执行代码中变量和函数的声明会作为环境记
    录(Environment Record)添加到变量环境中。
        对于函数来说,参数也被作为环境记录添加到变量环境中。
     

了解真相,才能获取真正的自由

    了解完上述概念后,我们首先要有这么一个理解, 在script标签中我们可以创建一些内置对象,使用内置对象的方法,也可以
    通过使用函数的功能,这些功能我们为什么能使用?
         原因很简单,就是因为有GO,GO从哪里来?也很简单,是由js引擎在编译过程中给我们创建的。我们常说的js中只有全局
    作用域和函数作用域,为什么,因为js引擎给我们创建了GO,AO,没有其他O。
        最后,当了解完这些概念之后,我们还要探索在js代码运行的过程中,这些概念是如何联系起来的,这样能使我们对这些
    概念的理解更上一层楼。
     

二: js引擎如何运行我们的代码?

背景知识:

     在探索js引擎运行代码的逻辑之前,我们首先要对js的内存管理有一定的理解,否则在某些概念的理解上会很吃力。
     有关概念,请移步[[js高级-内存管理]](https://juejin.cn/post/7089788055208329253)
 

js引擎运行代码的步骤

    js 引擎要运行我们的代码就要经历三个阶段。具体如下
        1.词法语法分析:词法语法分析就是检查JavaScript代码是否有一些低级的语法错误
        2.预编译:下文详细讲解。
        3.执行代码:执行代码就是js引擎解析代码,解析一行执行一行
    第一个阶段需要编译的知识,这里我们主要研究和分析后两个阶段。另外,在理解作用域的的提升和作用域链的相关概念时,我们
一定要清晰的把第二个过程和第三个过程区分开,不然对这些概念的理解就会出现很大的偏差。

    

预编译阶段详解

预编译分为两个时间点

  1.第一个是在JavaScript代码执行之前
  2.第二个是在函数执行之前。

  需要注意的是在JavaScript代码之前的时间点的预编译只发生一次,函数执行之前的预编译是多次的。

这里会将该段代码放在html文档结构的script标签中。

一:JavaScript代码执行之前的预编译

    这个阶段主要做了如下几件事情
     1.创建全局对象(GO),填充内容
         填充的内容主要分为四个部分:
          (1)DateArrayStringNumbersetTimeoutsetInterval 等js内置的对象及其方法
          (2)window,指向自身,并且可以在代码中全局访问
          (3)解析代码,将其中的的全局变量,未使用和let生命的变量填充到GO,并将其赋值为undefined(作用域提升)
          (4)解析代码,将其中的函数声明以key:value的形式,添加至GO中。(key:value形式存在,key为函数名,value为
          函数体的内存地址)

    2.创建全局执行上下文(GEC),并填充内容
          填充的内容主要分为两部分
           (1)填充变量环境(VO),这里的VO是一个变量,会将创建好的GO赋值给VO
           (2)填充函数执行体,这里的函数执行体的解析是在函数执行前的预编译中进行的,在这个阶段中并不会填充这部分内容,
           这里只是对GEC中的内容做结构展示
    3.创建执行上下文栈(调用栈)ECS
         创建执行上下文栈(调用栈)ECS,用来执行创建好的GEC

二:函数执行前的预编译

     1.创建AO对象
         函数执行前的一瞬间,生成AO活动对象
     2.分析生成AO属性
         查找形参和变量声明放到AO对象,赋值为`undefined`
     3.分析函数声明
        查找函数声明放到AO对象并赋值为函数体。函数名为属性名,值为函数体;
     4.生成函数执行上下文FEC,并填充内容
         填充的内容主要分三个部分
          (1)将AO对象作为VO,赋值给FEC的VO结构。
          (2)填充作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
          (3)this绑定的值:这个我会在后续文章中详细解析;
      

词法分析-编译-执行 流程总结

   这里再对代码的运行流程做一些解释,即将执行的三个步骤和预编译的两个结合起来,做一个统一解释。
       1.首先js代码的执行的第一步是做词法语法分析。
       2.然后进行预编译的第一个时间点,即javaScript代码执行之前的预编译。
       在这个时间点,进行如下几个步骤。
         (1)创建全局对象(GO),填充内容
         (2)创建全局执行上下文(GEC),并填充内容
         (3)创建执行上下文栈(调用栈)ECS
       3.接着开始执行代码,即从代码的第一行开始执行。
       如果没有遇到函数,就一直执行。
       4.如果遇到函数时,会来到预编译过程的第二个时间点,即函数执行之前的时间点。
       在这个时间点会进行如下的几个步骤。
          (1)将AO对象作为VO,赋值给FEC的VO结构。
          (2)填充作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
          (3)this绑定的值:这个我会在后续文章中详细解析;
        
       5.执行函数体,知道该函数执行完毕
       6.按照上述步骤继续执行全局代码,直到代码执行完毕。

下面根据我们总结的流程来对代码做详细的分析和画图总结

案例分析

    var name = "why"
    function foo(){
        var name ='foo'
        console.log(name)
    }
    var num1 = 20
    var num2 = 30
    var result = num1 + num2
    console.log(result)
    foo()
 1.首先js代码的执行的第一步是做词法语法分析。这里会生成AST语法树。
 
 2.然后进行预编译的第一个时间点,即javaScript代码执行之前的预编译。
  (1)创建全局对象(GO),填充内容,具体填充如下几个内容
    -   DateArrayStringNumbersetTimeoutsetInterval 等js内置的对象及其方法
    -   window,指向自身,并且可以在代码中全局访问
    -   将name,num1,num2,result添加到GO对象中
    -   将foo函数以foo:0xa00的形式赋值,添加到GO对象中
    该步骤完成后会在堆中存在一个这样的对象:
    

截屏2022-04-24 下午10.57.33.png

   (2)创建全局执行上下文(GEC),并填充内容
  

截屏2022-04-24 下午11.14.30.png

   (3)创建执行上下文栈(调用栈)ECS

截屏2022-04-24 下午11.15.26.png

3.接着开始执行代码,即从代码的第一行开始执行。如果没有遇到函数,就一直执行。

截屏2022-04-24 下午11.16.58.png

4.如果遇到函数时,会来到预编译过程的第二个时间点,即函数执行之前的时间点。

截屏2022-04-24 下午11.20.21.png

5.执行函数体,知道该函数执行完毕

截屏2022-04-24 下午11.21.13.png

6.按照上述步骤继续执行全局代码,直到代码执行完毕,由于执行完foo函数后没有其他代码,所以js代码运行结束。

变量的作用域的提升

作用域的提升,发生在预编译的第一个阶段。

作用域链的生成

作用域链的生成发生在预编译的整个过程中