JavaScript预编译简讲

215 阅读7分钟

 概要

JavaScript虽然是一门解释执行语言,不像Java一样需要打包成jar包或者编译后再执行,但是还是有提前编译的,才会引出概念如:变量提升,作用域,闭包等问题。

这篇文章可能对你的实际开发没有任何帮助,如果有了解这块分享的,可以帮忙纠错。

1:js运行的三步骤

分词/词法分析--->解析/语法分析(预编译)-→代码生成(解释执行)

(1)分词:顾明司仪就是如  var a = 2;  一般会被分解成var、a、=、2、;五个部分,取决于空格在这门语言中的意义。

(2)解析:这个过程一般是吧词法单元流(数组)生成一棵"抽象语法树”(AST)

(3)执行代码

2:预编译初窥

从下图几个例子代码中来看下预编译

(1)函数 声明整体提升

JavaScript是逐行执行的,正常来说这里执行应该是报test undefined,但是输出结果确实123,说明了代码一开始是有预编译环节的。这里预编译的时候

函数无论在哪里声明都会被提到逻辑最前面,所以不在在哪里调用,本质上执行的时候都是在声明后调用,

(2)变量 提升(声明提升)

这就是典型的预编译环节的变量提升,var定义的变量会先声明但不赋值,所以图2的时候输出是undefined,图3是输出10;

(3)特殊分析

(3)-1:未经声明的变量归全局(window)所有

(3)-2:声明的全局变量同样归window(全局作用域)所有

根据四句总结来解读上图代码:变量声明提升有了var a; 函数整体提升,a赋值了一个函数,然后console.log,可以理解为下面代码

3、预编译的作用

预编译实际是为了解决执行顺序问题,接下来就来理解一下执行过程发生了什么。

3.1:函数预编译分析

总共四步:

第一步:创一个AO对象(“Activation Object” 执行期上下文)

第二步:找形参和变量声明,将变量名和形参作为AO对象的属性名,先赋值undefined,同名取一次即可,相当于:

AO {

a:undefind,

b:undefind

}

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

AO {

a:1,

b:undefind

}

第四步:在函数中找到函数声明,值赋上函数体,名称如果存在则不新建赋值覆盖就好

AO {

a:function a(){},

b:undefined,

d:function d:(){}

}

然后我们结合预编译后的AO开始执行代码

第一次打印的a 就是AO里面a的值: function a(){}被打印出来,

var a =123 ,AO中的值变成123,第二次打印的就是AO里现在的值123,(这句代码中预编译看过的就不看了,预编译中变量声明已经做过了,所以只执行了后半句“=123”);

function a(){} 不看因为第四步的时候已经预编译过了

第三次打印AO里的a还是123

给b赋值function(){},这时候AO里b的值就变成了function(){},然后执行打印,打印出来的就是function(){}

看到这里应该对函数预编译有了大致的框了吧。

再来举个例子

第一步:创一个AO对象(“Activation Object” 执行期上下文)

AO{

}

第二步:找形参和变量声明,将变量名和形参作为AO对象的属性名,先赋值undefined,同名取一次即可,相当于:

AO {

a:undefind,

b:undefind,

c:undefind

}

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

AO {

a1,

b:undefind,

c:undefind

}

第四步:在函数中找到函数声明,值赋上函数体,名称如果存在则不新建赋值覆盖就好

AO {

a1,

b:function b() {},

c:undefind,

d:function d() {}

}

*函数预编译是执行在函数执行前。

3.2 全局预编译

其实和3.1提到的过程有点类似,但只有三步,一样举个例子。

第一步:生成GO对象 (“Global Object”)其实可以理解为window对象

GO{

}

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

GO{

a:undefind

}

第三步:找函数声明,函数名作为属性名,值为函数体

GO{

a:function a() {}

}

4、AO与GO结合

思考:先有AO还是先有GO?

肯定是先有GO,上面提到了AO是在函数执行前出现的。

来看下面这个代码:

​编辑

我们一步步来

①:肯定是生成了一个GO

GO{

}

②:我们找到全局的形参和变量声明,变量名和形参名作为GO的属性名,值为undefined

GO{

test:undefind

}

③:找函数声明,函数名作为属性名,值为函数体

GO{

test:function test(){

此处省略中间的代码...

}

}

④:然后我们开始执行显示输出test,就是GO中的test

⑤:执行test(1),此时函数执行前我们要进行函数预编译生成一个AO

GO{

test:function test(){

此处省略中间的代码...

}

}

AO{

}

⑥:找形参和变量声明,将变量名和形参作为AO对象的属性名,先赋值undefined

GO{

test:function test(){

此处省略中间的代码...

}

}

AO{

test:undefind

}

⑦:将实参值和形参值统一

GO{

test:function test(){

此处省略中间的代码...

}

}

AO{

test:1

}

⑧在函数中找到函数声明,值赋上函数体

GO{

test:function test(){

此处省略中间的代码...

}

}

AO{

test:function test(){}

}

⑨:此时GO和AO都有test属性,执行test函数的时候发现AO和GO都有属性test,此时就近原则先找AO,AO没有再找GO

所以执行函数

先是打印出:test(){}

给test赋值234,打印出234

5:总结

预编译的作用就是让你知道如何捋清楚代码的执行顺序,开发过程中如果遇到一些值和你预想的不一样,都可以有一条解决的思路。

主要需要记住的就是以下几点

第一块:这里需要记住区分声明和赋值是分开的
1:函数 声明整体提升

2:变量 提升(声明提升)

3:未经声明的变量归全局(window)所有

4:声明的全局变量同样归window(全局作用域)所有

第二块:函数预编译

这里主要是要记得执行的四个步骤和触发时机是函数执行前

1:创一个AO对象(“Activation Object” 执行期上下文)

2:找形参和变量声明,将变量名和形参作为AO对象的属性名,先赋值undefined,同名取一次即可,相当于:

3:将实参值和形参值统一

4:在函数中找到函数声明,值赋上函数体,名称如果存在则不新建赋值覆盖就好

第三块:全局预编译

1:生成GO对象 (“Global Object”)其实可以理解为window对象

2:找形参和变量声明,变量名和形参名作为GO的属性名,值为undefined

3:找函数声明,函数名作为属性名,值为函数体

先生成GO再生成AO,AO和GO是一个链式关系,执行函数的过程中,先经过AO,AO没找到找GO,GO找不到的时候报错