深入理解 JavaScript 预编译:变量提升与函数提升的底层原理

28 阅读4分钟

上期我们讲到v8引擎的工作原理:将源代码字符串拆分成一个个具有特定含义的词法单元将分词得到的词法单元组织成一棵 AST(抽象语法树),然后根据AST生成代码并执行代码。那么在js当中预编译又是什么机制呢?

一、什么是预编译?

预编译是指 JavaScript 代码在正式执行之前,会先进行编译处理。这个过程发生在代码执行之前,而不是等到代码运行到某一行时才进行。

二、函数体内的预编译

当函数被调用时,会在函数体内发生预编译。这个过程中,进行了如下几个步骤:

  1. 创建一个执行上下文 ,设为AO:{}
  2. 找形参和变量声明,将形参和变量名作为属性名,添加到AO中,值为undefined
  3. 将形参和实参统一
  4. 在函数体内找函数声明,将函数名作为AO属性名,函数体作为属性值,添加到AO中

我们举一个例子

function fn(a) {
  console.log(a);//function a() {}
  var a = 123//变量声明
  console.log(a);//123
  function a() {}//函数声明
  var b =function() {}//变量声明
  console.log(b);//function b() {}
  function c() {}//函数声明
  var c=a//变量声明
  console.log(c);//123

}
fn(1)//实参1

上述代码在预编译的过程中:

  1. 先创建了一个类AO:{},然后我们发现形参有a,且声明了a,b,c三个变量,因此AO={a:undefined, b:undefined, c:undefined}
  2. 将形参和实参统一,实参是1,此时AO={a:1, b:undefined, c:undefined}
  3. 接下来我们看函数声明,上述代码声明了ac 两个函数,我们将函数体作为属性值添加到AO中,此时 AO={a:function a() {}, b:undefined, c:ufunction c() {}}
  4. 随后的执行过程中,在函数体内从上往下执行,先console.log(a) ,此时a的值是function a() {} ,因此第一个输出结果为function a() {},接下来a = 123 ,此时 AO={a:123, b:undefined, c:ufunction c() {}} 因此第二个输出结果为123
  5. 接下来从又为b赋值为函数 ,所以 AO={a:123, b:function b() {}, c:function c() {}} ,因此第三个输出结果为function b() {}
  6. 随后,又为变量c 赋值为a ,由于此时a的值为123,所以 AO={a:123, b:function b() {}, c:123} ,故第四个输出结果为123

三、全局的预编译

当 JavaScript 文件开始运行时,会先进行全局级别的预编译。全局预编译相较于函数体内的预编译较为简单,过程如下:

  1. 创建一个全局执行上下文 设为GO:{}
  2. 找全局变量声明,将全局变量名作为属性名,添加到GO中,值为undefined
  3. 找全局函数声明,将全局函数名作为属性名,函数体作为属性值,添加到GO中 同样的,举一个例子
var a
var b = 2
function a () {
  console.log(a); //undefined
  var c = 3
  var a = b
  function c() {}
  console.log(c);//3
}
a()
console.log(a)//function a(){}

上述代码在预编译时,我们暂且不看函数a内部:

  1. 创建一个全局执行上下文GO:{},全局变量声明了ab两个变量,此时GO={a: undefined, b: undefined}
  2. 我们发现还声明一个函数function a (){} ,函数名作为属性名,函数体作为属性值,此时GO={a:function a (){} , b: undefined}
  3. 随后在代码执行时,将变量b赋值为2,此时GO={a:function a (){} , b: 2} ,然后a() 调用函数a ,此时又将在函数体内发生预编译,我们接下来看函数体内部
  4. 创建AO:{},函数内声明了变量ca,此时AO={c: undefined, a: undefined}
  5. 且声明了函数 c ,函数名作为AO属性名,函数体作为属性值,此时AO={c: function c() {}, a: undefined}
  6. 同样的,执行函数时,先console.log(a),此时a的值为undefined,故第一个输出结果为undefined
  7. c赋值为3,a赋值为b ,又因为在GOb的值为2此时AO={c: 3, a: 2} ,随后打印c,故第二个输出结果为3
  8. 函数执行完后,运行console.log(a) ,因为该语句在函数体外,会先在全局中寻找变量a,由于在全局GOa的值为function a (){} ,故第三个输出结果为function a (){}

四、变量提升与函数提升

变量提升(Hoisting)

使用 var 声明的变量会被提升到当前作用域顶部,但赋值不会被提升:

console.log(a);  // undefined(提升但未赋值)
var a = 1;
console.log(a);  // 1

上述代码实际执行顺序:

var a;           // 提升声明
console.log(a);  // undefined
a = 1;
console.log(a);  // 1

函数提升(Hoisting)

函数声明会被完整提升到当前作用域顶部:

say();  // "Hello"
function say() {
    console.log("Hello");
}

但函数表达式不会被完整提升:

say();  // TypeError: say is not a function
var say = function() {
    console.log("Hello");
}