美文网首页细品 JavaScript
深入了解JavaScript从预编译到解析执行的过程

深入了解JavaScript从预编译到解析执行的过程

作者: 越前君 | 来源:发表于2020-03-22 23:49 被阅读0次

    先来一个最简单的案例。

    var a = 1;
    

    从字面上看,这就是简单的将 1 赋值给变量 a。可在 JS 引擎里面,它认为这是两个步骤:var aa = 1,分别是声明和赋值,它们发生在两个不同的阶段。

    写这篇文章的原因是看到一道题,发现自己对预编译的理解出现了偏差。加上以往也没整理过,久了不接触就会慢慢遗忘、凌乱,所以借此机会整理下预编译的知识点,同时希望这篇文章能帮助屏幕前的你。

    一、JS 从加载到执行完经历了什么?

    1. 页面产生便创建了一个 GO 全局对象(Global Object,全局上下文),即 window 对象。
    2. 第一个脚本文件加载。(加载完成后,接着是 JS 运行三部曲)
      1). 语法分析
      2). 预编译
      3). 解析执行
    3. 三部曲第一步:语法分析,检查是否合法。
    4. 三部曲第二步:开始预编译:
      (其实这里还有创建了 document、navigator、screen 等属性,此处忽略)
      1). 查找变量声明,作为 GO 属性,并赋予 undefined;
      2). 查找函数声明,作为 GO 属性,并赋予函数体本身(若函数与变量同名,函数会覆盖变量;若多个函数声明是同名,后面的会覆盖前面的)
    5. 三部曲第三步:开始执行代码。
    6. 执行完一个 script 块,到下一个 script 块,会重复 2、3、4、5 步骤。
    <script>
      console.log(a); // function a() {}
      console.log(b); // function b() {}
      console.log(c); // undefined
      var a = 1;
      function a() {};
      function b() {};
      var c = 3;
      console.log(a); // 1
      console.log(d); // Uncaught ReferenceError: d is not defined,且从这里开始代码终止执行
      d = 4;
      console.log(d);
      /**
      (有必要说明一下:下面的 GO 是伪代码,为了助于理解罢了)
      1. 创建 GO 对象;GO {}
      2. 加载 script 块
      3. 语法分析
      4. 开始预编译
        1). 找到变量声明 a 和 c,放到 GO {a: undefined, c: undefined}
        2). 找到函数声明 a 和 b,因为函数 a 和 变量 a 同名,所以函数覆盖掉变量:GO {a: f(), b: f(), c: undefined}
      5. 执行代码
        1). 执行到 console.log(a),然后从 GO 里面找到 a,所以打印出来是函数;
        2). 继续往下执行,同理打印 b 也是函数;
        3). 同理,c 打印结果是 undefined;
        4). 因为 a 赋值 1,所以第二个 console.log(a) 会打印 1;
        5). 到了 console.log(d) 这一步,因为暗示全局变量 (imply global variable),不参与预编译的过程,所以会报引用错误;(代码终止执行,不会继续往下走)
      */
    </script>
    

    JavaScript 不是全文编译完再执行,而是块编译,即一个 script 块中预编译然后执行,再按顺序预编译下一个 script 块再执行。但是此时上一个 script 块中的数据都是可用的,而下一个 script 块中的函数和变量则是不可用的。

    二、解析执行阶段,当遇到函数执行的时候,会产生 AO 活跃对象(Activation Object,活跃上下文),那过程又是怎样的呢?

    首先要明确一点,预编译不仅仅发生在 script 代码块执行之前,还发生在函数执行之前

    函数执行之前的“预编译”的过程:

    1. 创建 AO 对象
    2. 查找形参和变量声明,并赋予 undefined;
    3. 实参值赋给形参;
    4. 查找函数声明,赋予函数本身;

    预编译完了之后,接着是执行函数。(下面举例说明下这个过程)

    <script>
      function fn(a) {
        console.log(a); // 123
        console.log(b); // undefined
        console.log(c); // function c() {}
        var a = 1;
        var b = function() {}; // 注意:函数表达式,其实就是将一个匿名函数赋值给变量 b
        function c() {};
        console.log(a); // 1
        console.log(b); // function() {}
      }
      fn(123);
      /**
        有必要说明一下:下面的 AO 是伪代码,为了助于理解罢了
        1. 创建 AO 对象;AO {}
        2. 预编译
          (还包括 arguments 等,此处忽略)
          1). 查找形参和变量声明 a、b 和 c,放到 AO {a: undefined, b: undefined}
          2). 实参赋值给形参:AO {a: 123, b: undefined}
          3). 找到函数声明 c,AO {a: 123, b: undefined, c: f()}
        3. 执行代码
          1). 执行到 console.log(a),然后从 AO 里面找到 a,所以打印出来是 123;
          2). 继续往下执行,打印 b 是 undefined;
          3). 继续往下,a 赋值 1;b 赋值一个匿名函数;
          4). 第二个 console.log(a) 会打印 1;
          5). 第二个 console.log(b) 会打印 function;
      */
    </script>
    

    三、其他

    其实 GO 和 AO 就差在形参和实参这俩个东西。这也是我们常说的“提升” (Hoisting)。

    其实这些蛋疼的情况,几乎只会出现在“面试”上,在实际的项目中几乎很少出现。如果在多人共同维护项目中,估计早被打死了。因为可读性差而且维护成本高。

    在 ES6 中,新增的 let、const、class 特性就不会存在提升的问题,声明变量,必须要在调用之前发生,否则就会报错。这应该也是一个信号,减少那些蛋疼的情况。

    但是无论是为了对付面试,还是为了了解 JavaScript 内部运行的原理,作为前端开发的一员,我们都应该要弄清楚。

    相关文章

      网友评论

        本文标题:深入了解JavaScript从预编译到解析执行的过程

        本文链接:https://www.haomeiwen.com/subject/jcopyhtx.html