JavaScript---预编译以解释声明提升

167 阅读5分钟

前言

在语言学习中有时了解底层原理比记忆一些知识点特性要有效的多,了解底层原理也可以更方便理解这门语言,而且不论代码任何变化,都可以依据底层原理来一句一句解释。

什么是预编译

预编译就是就是js执行的第二个步骤,js执行总共分为三个步骤:
1.语法分析:   V8引擎在解析js代码之前,会先通篇进行扫描,若有语法、逻辑错误,报错并停止执行。

2.预编译: 语法和语句全部会被转换成对象,GO(Global Object),AO(Active Object)把代码按照 一定的规则,放到GO和AO中。GO及AO在代码执行完后会被销毁。

3.解释执行: 编译一行执行一行,当语法分析没有问题,并且已经完成预编译阶段之后,就开始解释执行代码。

在讲解预编译过程前,先讲解下声明提升

使用var关键字声明的变量存在声明提升。在代码执行前阶段,变量声明会被移动到作用域的顶部(详细可见我另外一篇有关作用域的文章),但赋值操作保留在原来的位置。

例如

console.log(a); // 输出:undefined
var a = 1;

按照正常代码从从上往下运行理解,应该会报错才对,没有找到变量a。结果输出是undefined,a没有定义,这说明a已经被声明了。所以代码效果看起来和下面代码一样,变量声明提升了,赋值声明留在原地。

var a ;
console.log(a); // 输出:undefined
 a = 1;

并且,函数也存在声明提升,并且会整体提升

foo(); 
function foo()
{ 
console.log("nihao"); // 打印nihao
}

按正常逻辑思维来,函数调用在函数声明前,代码怎么看都运行不起来,函数调用找不到函数,怎么就能打印结果呢。 这说明,在该作用域内函数的声明提升到了调用前。代码效果起来和下面代码一样。

function foo()
{ 
console.log("nihao"); // 打印nihao
}
foo(); 

函数中未定义的变量,默认将其定义在全局中。

function foo() {
	var a = 1
	b = 2
  	console.log(a) // 1
  	console.log(b) // 2
}
foo()
console.log(a) // 报错:a 是 foo() 中的变量,全局中无法找到(作用域的一个问题,可以看我其他文章)
console.log(b) // 2   b = 2 扩散到全局了

那下面的代码到底输出什么?既有函数又有变量,而且还同名,那变量的声明提升和函数提升到底谁的优先级高呢?想必是很疑惑吧,没关系,跟着我一起来通过预编译一行一行理解下。

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() {}
 }
     fn(1);

预编译过程

 因为执行环境不同,所以分为全局环境预编译和函数环境预编译:

1.函数环境预编译

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

2.全局环境编译

  1. 创建GO对象 (Global Object)
  2. 找变量声明,将变量声明作为GO对象的属性名,值赋予undefined
  3. 在全局找到函数声明,将函数名作为GO对象的属性名,值赋予函数体
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() {}
 }
     fn(1);

因为是函数环境,首先先创建OA对象

AO{
}

再找形参和变量声明,将形参和变量声明作为AO的属性名,赋予undefined

AO{ 
a:undefined, 
b:undefined,
}

再将形参和实参统一,就是找到实参的值,赋值给形参

AO{
a:1,  
b:undefined
}

最后在函数体内找到函数声明,将函数名作为AO对象的属性名,值赋予函数体。函数体预编译就这样完成了

AO{
a:function a() {},//函数声明
b:undefined, //注意了 var b = function () {}这个不是函数声明,是函数表达式,所以b的值还是undefined 
d:function d() {}//函数声明
}

同一变量在同一作用域中被多次定义时一般会被覆盖修改。

此时AO对象已经创建完,那么就开始执行函数,开始打印。

 function fn(a) 
 {    
        console.log(a);// 输出function a() {}
        var a = 123;//预编译时已经变量提升了,但是赋值还没有操作。所以此时AO对象中a的值改变为123
     // AO{
     // a:123,
     // b:undefined,
     // d:function d() {}
     //} 
         console.log(a); // 输出:123
         function a() {}  
         console.log(a); //输出123
         var b = function () {} // 改变AO对象中b的值
     // AO{
     // a:123,
     // b:function () {},
     // d:function d() {}
     //}       
         console.log(b); //6.输出function () {}
         function d() {}
 }

结果如图,确实与推算一致

image.png

我们再来看看下面这个代码,

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

预编译后

// GO = {
//     global: undefined,
//     fn= function fn() {},
// }
global = 100
function fn() {
    console.log(global);  // undefined
    global = 200
    console.log(global); // 200
    var global = 300    
}
fn()
// AO = {
//     global:undefined ,
// }
var global;

大家可以自己试着预编译实践看看,看看和我预编译的是否一致,并且验证打印结果是否一致