概要
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找不到的时候报错