小白也能轻松get的JavaScript预编译原理

158 阅读4分钟

前言

JavaScript的编译过程中会经历以下三个步骤:

  1. 语法分析
  2. 预编译
  3. 解释执行

在许多大厂的面试中,JavaScript的预编译原理是一个经典问题,本文将会带大家了解预编译的具体流程。在这之前,我们需要先了解声明提升是什么!

声明提升

我们先来看一段代码:

var a = 100;
console.log(a);

大家肯定觉得这太简单了,不就是输出100吗。

a4d569554a8d2583bda55916bc0fab25(1).jpg

如果修改一下代码

console.log(a);
var a = 100;

这看起来好像不符合我们正常思维逻辑,怎么变量a先输出后声明啊?这样会输出什么结果呢?

image.png

输出的是undefined。这是为什么?因为var 声明的变量会存在声明提升

上面这段代码等价于

var a;
console.log(a);
a = 100;

这就好像变量声明从它在代码中出现的位置被“移动”到了最上面。

不仅是变量,函数也存在提升,但是函数声明会存在整体提升。

fun();
function fun(){  
    var a = 111;
    console.log(a);
}

看到这里大家肯定很兴奋,这就和之前的例子一样,console.log(a)输出111,我已经学会了!

0bef8ece03e1fbfa08ae1fa11412907e(1).jpg

那么我们现在增加亿点点难度,请写出以下代码的输出结果。

function fn(a) {
    console.log(a); //
    var a = 123
    console.log(a); //
    function a() {}
    console.log(a); //
    var b = function() {} 
    console.log(b); //
    function d() {}
    var d = a
    console.log(d); //
  }
fn(1)

现在你能通过声明提升的知识来解决这道题吗?当然不行,这不是我们所需要的最优解法。那么现在来进入我们今天的正题:JavaScript的预编译原理

预编译

预编译分为函数体内的预编译和全局作用域内的预编译。

关于作用域的概念和分类可以移步到我的另一篇文章:juejin.cn/post/729451…

函数体内的预编译

总共分为四步:

  1. 创建函数的AO对象(Action Object)
  2. 找形参和变量声明,将形参和变量声明作为AO的属性名,值赋予undefined
  3. 将形参和实参统一
  4. 在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体

我相信你现在心里肯定有一个大大的问号,这人在干嘛,写的什么玩意?

v2-2c1280a626bc3fa8857cb8444e132d74_r(1).jpg

别急,听我给你娓娓道来。按照上面的例子一步一步写下来,相信看完你会惊叹这题原来如此简单。

function fn(a) {
    console.log(a); //
    var a = 123
    console.log(a); //
    function a() {}
    console.log(a); //
    var b = function() {} 
    console.log(b); //
    function d() {}
    var d = a
    console.log(d); //
  }
fn(1)

1、我们首先创建一个AO对象

AO:{
               
}

2、找到函数体内的形参和变量声明,全部赋予undefined

AO:{
   a:undefined
   b:undefined
   d:undefined
}

此处有一个易错点,第七行并不是一个函数声明而是一个函数表达式,将function() {}这个函数赋值给已经声明的变量b。

3、将形参和实参统一:用实参的1覆盖a的undefined

AO:{
   a:undefined -> 1
   b:undefined
   d:undefined
}

4、在函数体内找函数声明,值赋予函数体

AO:{
   a:undefined -> 1 -> function a() {}
   b:undefined -> function() {}
   d:undefined -> function d() {}
}

预编译完成,现在我们开始执行语句。

第二行console.log(a)输出function a() {}

第四行console.log(a)输出123,因为前面的赋值语句将a的值修改为123

第六行console.log(a)还是输出123

第八行console.log(b)输出function b() {}

最后一个console.log(d)输出123

image.png

怎么样,经过这四个步骤层层推进,是不是觉得这道题简直是小儿科~

71e345acbf3df13d0e726fd28c11df6b(1).jpg

全局作用域内的预编译

总共分为三步:

  1. 创建GO(Global Object)对象
  2. 找变量声明,将变量名作为GO的属性名,值赋予undefined
  3. 在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体

其实我们不难发现,这两种预编译的差别不大,只是全局的预编译少了型参与实参的步骤。

先来道例题练练手:

    global = 100
    function fn() {
      console.log(global); // 
      global = 200;
      console.log(global); // 
      var global = 300;
    }
    fn();
    var global;

第一步创建一个GO对象:

GO:{
              
}

第二步找到变量声明,值赋予undefined:

GO:{
    global:undefined
    fn:undefined
}

第三步找到函数声明,值赋予函数体

GO:{
    global:undefined,
    fn:function fn() {},
}

现在我们发现了一个函数,那么还要按照上述的步骤来对其进行分析,大家可以自己尝试一下,在此我就不再赘述了。

AO: {
      global: undefined -> 200 -> 300
}

第三行输出undefined

第五行输出200

    console.log(global); //undefined
    global = 200
    console.log(global); //200

结语

至此,你已经学会了JavaScript的预编译原理。相信你在今后的面试中遇到这种类型的题目能够胸有成竹地拿下这一分!