先有鸡还是先有蛋——浅析javascript中代码的编译

784 阅读4分钟

先有鸡还是先有蛋?

我们直觉上一般认为JavaScript代码在执行时是由上到下一行一行执行的,但是实际上这并不完全正确,考虑以下代码:

           a = 2;

            var a;

            console.log(a);//输出2

初学者可能会认为输出undefined,因为var a 声明在a=2之后,自然容易得出变量被重新赋值,但是真正的输出却是2.

考虑另一段代码

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

鉴于上一个代码片段表现出来的某种非自上而下的行为特点,你可能认为这个代码会输出2,亦或是抛出ReferenceError异常,然而结果是输出undefined。

编译器来袭

为了搞明白这个问题,我们需要回顾编译器的内容,引擎会在解释JavaScript代码之前首先对它进行编译,编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来,因此,正确的思路应该是包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理

所以当你看到var a = 2时,JavaScript实际上会将其看成两个声明:var a和a=2,第一个声明是在编译阶段进行的,第二个赋值会被留在原地等待执行阶段。

我们第一个代码片段会以如下形式进行处理:

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

其中一部分是编译,而第二部分是执行。类似的,我们的第二个代码片段实际是按以下流程处理的:

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

因此,这个过程就像变量和函数声明从他们在代码出现的位置被“移动”到了最上面。这个过程就叫提升。 换句话说,先有蛋(声明),后有鸡(赋值)。

预编译

严格的来说,js中并没有预编译的说法,按我个人的理解,简单谈谈我的想法:

预编译什么时候发生?

预编译不仅仅发生在全局1代码执行之前,在函数执行前的那一刻也会发生预编译。那么全局预编译和函数预编译会一样吗?

函数预编译

我们考虑以下代码:

    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)

分析:

第一步:创建一个AO对象(Activation Object):

AO:{

}

第二步:找形参和变量声明,将形参和变量声明作为Ao对象的属性名,值为undefined:

AO:{

a:undefined,

b:undefined,

d:undefined

}

第三步:将实参和形参统一:

AO:{

  a:undefined -> 1
  
  b:undefined,
  
  d:undefined

}

第四步:在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体:

AO:{

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

}

预编译结束后,AO对象为:

AO:{

a: function(){},

b:undefined,

d:function(){}

}

预编译阶段结束,代码进入执行阶段,开始一行一行的执行代码:

  1. 执行console.log(a),输出function(){},
  2. 执行a=123,执行consile.log(a),输出123
  3. 遇到函数声明,并未调用,所以跳过它,执行下一行console.log(a),输出123
  4. 执行函数表达式,输出function(){}
  5. 遇到函数声明,并未调用,跳过,执行下一行,将a赋值给d,输出123

总结

结论:

1. 创建一个AO对象(activation object)

2. 找形参和变量声明,将形参和变量声明作为Ao对象的属性名,值为undefined

3. 将实参和形参统一

4. 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体

AO对象改变过程:

AO:{

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

}

全局预编译

考虑以下代码:

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

分析:

第一步:创建Go对象: GO:{

}

第二步:找变量声明,将变量声明作为GO对象的属性名,值赋予undefined:

GO:{

global:undefined

}

第三步:找全局里的函数声明,将函数名作为Go对象的属性名,值赋予函数体:

GO:{

global:undefined
fn:function(){}

}

其他:在函数作用域中,还创建了AO对象:

AO:{}

预编译阶段结束后,GO对象为:

GO:{

global:undefined
fn:function(){}

}

预编译阶段结束,代码进入执行阶段,开始一行一行的执行代码:

  1. global被赋值为100
  2. 输出global

总结

1. 创建Go对象

2. 找变量声明,将变量声明作为GO对象的属性名,值赋予undefined

3. 找全局里的函数声明,将函数名作为Go对象的属性名,值赋予函数体

最后

JavaScript初学者,若文章有误还请大佬指正。