从底层认识JavaScript的预编译

558 阅读3分钟

一、前言


JavaScript运行代码总结下来就是三步,即分析代码预编译执行语句。分析代码就是对代码排查一些错误;预编译就是在执行代码前对变量、函数等声明;执行语句就是从上往下执行代码。本文主要对第二步预编译进行分析,希望可以帮助到大家!

二、代码是怎样运行的


1. 在执行过程中,若使用未声明的变量,js执行会报错。

例如:

console.log(a);  // a 在执行前没有声明,执行会报错
a = 123;  // 赋值

2. 在一个变量定义之前使用它,不会报错,但该变量的值为undefined,而不是定义的值。

例如:

console.log(a);  // 执行时不会报错,a 的值为 undefined,而不是123
var a = 123;  // 定义并赋值

3. 在一个变量定义之前使用它,是不会报错的,且函数能正常执行。

例如:

console.log(a);  // 值为 undefined
foo(); // 结果为 hello

var a = 123;
function foo() {
    console.log('hello');
}

为什么代码执行会出现这些现象呢?代码执行的顺序到底是怎么样的呢?要解释这些问题,下面我们引入变量提升。

三、变量提升


JavaScript 代码在执行过程中,JavaScript 引擎会把变量声明部分函数部分提升到代码的最前面的"行为"。变量提升发生在代码编译的时候。(这里提到的变量提升包括变量提升和函数提升)

先看下面这段代码:

function foo() {
    console.log(a);  // undefined
    var a = 123;
}
foo();

上述代码中 a 的结果是undefined,它的实际执行顺序如下:

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

接下来我们看一道经典的面试题:

console.log(a); // undefined
var a = 100;
function foo(){
    console.log(a); // undefined
    var a = 200;
    console.log(a); // 200
}
foo();
console.log(a); // 100

输出的结果为:

// undefined
// undefined
// 200
// 100

相信大家对变量提升已经有了一定的了解,那么问题来了,在变量提升的过程中,是先提升的在前面,还是后提升的在前面呢?是变量声明提升在前面还是函数提升在前面?下面我们就引入今天的主角预编译

四、预编译


1.预编译发生在函数体里时(四部曲)

  • 创建AO对象(Activation Object);
  • 找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined;
  • 将实参和形参的值统一;
  • 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体。

来看下面这个例子:

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

在函数执行之前会发生预编译,也就是在 fn(1) 之前会创建一个 AO 对象,fn(1) 执行时 AO 对象有如下变化过程:

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

代码执行时,先执行 AO 对象,然后从上往下执行代码,最后代码的执行结果为:

// [Function: a]
// 123
// 123
// [Function: b]

2.预编译也发生在全局 (三部曲)

  • 创建GO对象;
  • 找形参和变量声明,将变量声明和形参作为GO的属性名,值为undefined;
  • 在全局里找函数声明,将函数名作为GO对象的属性名,值赋予函数体。 来看下面这段代码:
a = 100
function fn() {
  console.log(a);  //undefined
  a = 200;
  console.log(a);  //200
  var a = 300;
  console.log(a);  //300
}
fn();
var a;

先创建一个 GO 对象

GO:{
    a: undefined  --> 100
    fn: function fn(){}

在函数体里创一个 AO 对象

AO:{
    a: undefined
}

执行时先执行 AO 在执行 GO,然后再按从上到下执行代码。
最后的执行结果为:

// undefined
// 200
// 300

以上就是预编译的一些底层原理!希望对大家有用,谢谢支持!