JS预编译原来可以这么简单

1,102 阅读6分钟

引言

JavaScript是解释型语言,这就导致很多同学认为JS引擎拿到一段JS代码之后就立马开始解释一行执行一行。其实这种认知是有误差的,下面通过一些例子来分析JavaScript代码执行的几个阶段。

console.log(a);
console.log(a); // 这里是中文的分号
console.log(a);

分析:上面这段代码只会报语法错误Uncaught SyntaxError: Invalid or unexpected token。这看起来好像没什么问题,但仔细一想变量a并没有定义,第一行代码是没有语法错误的,那么不应该报引用错误吗。这就说明JS引擎并不会立马执行代码,而是首先会通篇的检查代码中是否存在低级的语法错误,如果存在语法错误就停止后面的执行了。那我们是不是可以认为JavaScript代码的执行分两步,第一步是语法分析,第二步就是执行代码。其实中间还少了一步,这一步就是预编译,所以完整的过程应该是:

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

预编译什么时候发生

预编译不仅仅发生在全局代码执行之前,在函数执行前的那一刻也会发生预编译。好了,现在知道了预编译发生的时机,那么预编译会做一些什么工作呢?在全局预编译和函数预编译中做的工作会一样吗?下面通过实例来一一揭开预编译的真面目。

全局预编译

console.log(a);   // ƒ a(){}
function a(){};
var a = 1;

上面这段代码会打印a函数,这时候你可能会非常的疑惑,为什么呢?其实这就是预编译后代码的运行结果了。全局预编译的过程:

  1. 创建GO(Global Object)全局上下文
  2. 寻找变量声明,然后添加到GO对象中并赋值undefined
  3. 寻找函数声明,然后添加到GO对象中并复制函数体

现在知道了全局预编译的过程,我们来分析一下这段代码全部的运行过程,就知道为什么是这个结果了

  1. 预编译开始
  2. 创建一个GO对象,GO = {}
  3. 将变量a添加到GO中,GO = { a: undefined }
  4. 将函数声明a添加到GO中, GO = { a: function(){} },
  5. 至此预编译的过程结束
  6. 一行一行地开始执行代码
  7. 第一行:执行代码console.log(a);,这是GO中的a是一个函数,所以打印出函数a
  8. 第二行:声明一个函数,由于函数声明已经在预编译阶段做过了,不会执行。
  9. 第三行:将变量a赋值为1

关于上面这个例子可能很多同学会有疑问,为什么a打印出来不是1呢?这个问题上面地分析已经很直观了。因为打印a的这行代码在a赋值为1的这行代码之前啊。而这时a里面存的还是函数,自然就先打印出函数了。如果在最后添加一行代码console.log(a);,那么这个打印出来就是1了,不信,你试试。

小练习

来做几道题目,看看你是否理解了。答案在评论区揭晓,也欢迎各位积极讨论哦。

第一题

下面代码的运行结果是?

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

第二题

console.log(b)
function b(){}
var b = 1;
console.log(b)
b = function() {}

暗示全局变量

暗示全局变量就是,未经声明就赋值的变量,那么该变量就是一个全局变量,会被作为Window对象的属性。需要的注意的是不管赋值的位置,只要未声明就赋值,那么这就是一个全局变量。

a = 1  // 全局变量
function test() {
    b = 2  // 全局变量,即使在函数中赋值
}
test()
console.log(window.a) // 1
console.log(window.b) // 2

函数预编译

函数预编译和全局预编译差不多,都是在执行具体代码前做一些事情。函数预编译发生在函数执行前。在全局预编译中有一个GO对象,那么在函数预编译过程中也有一个AO(Active Object)对象,也是用来保存作用域中的变量和函数的。函数预编译有这么几个阶段:

  1. 创建AO活动对象
  2. 寻找形参,并将实参的值赋值给形参
  3. 寻找变量声明,并赋值为undefined
  4. 寻找函数声明,并赋值为函数体

还是通过一个例子,来具体走一遍这个代码的执行过程

function test() {
    console.log(b);
    if(a) {
        var b = 2;
    }

    c = 3;
    console.log(c);
}
var a;
test();
a = 1;
console.log(a);

分析:

  1. 创建GO对象,GO = {}
  2. 找到一个变量声明a,GO = { a: undefined }
  3. 找到一个函数声明test,GO = { a: undefined, test: function() {...} },
  4. 紧接着test函数被执行,执行之前的那一刻进入函数预编译阶段
  5. 创建AO活动对象
  6. 在函数中找到一个变量声明b, AO = { b: undefined }
  7. 变量c虽然在函数中被赋值,但是并没有使用var声明,所以是全局变量,被添加到GO对象中,GO = {a: undefined, test: function() {...}, c: undefined}
  8. 函数的预编译就结束了,开始执行函数内的代码了
  9. 打印变量b,此时变量b的值是undefined,所以打印出undefined
  10. 在函数的AO对象中没有声明变量a,所以就会到全局GO对象中找变量a。此时全局GO对象中的a是undefiend,所以条件不成立,b并没有被赋值,保持undefined。
  11. c被赋值为3,接下来打印出3
  12. 函数执行完毕,接着直接执行全局代码,a被赋值为1
  13. 接下来打印变量a,打印出1

小练习

第一题

a = 1;
function test(e) {
    function e(){}
    arguments[0] = 2;
    console.log(e);
    if(a) {
        var b = 3;
    }
    var c;
    a = 4;
    var a;
    console.log(b);
    f = 5;
    console.log(c);
    console.log(a);
}
var a;
test(1);

第二题

function test() {
    a = 1;
    function a(){}
    var a = 2;
    return a;
}
console.log(test());

总结

  • 预编译简单来讲就是在代码执行之前所做的一些准备工作。
  • 预编译发生在具体代码执行之前,不仅发生在全局,也发生在函数中。也就是全局预编译和函数预编译。
  • 暗示全局变量就是未经声明就赋值的变量,不声明就赋值相当于一种暗示。
  • 全局预编译和函数预编译的过程都差不多,函数预编译多了一个将实参赋值给形参的过程。
  • 在AO中找不到函数或者变量时,会到GO中去找。(闭包除外)