JS运行步骤
JS有两个特性,一个是单线程,一个是解释性语言。不同于编译性语言,解释性语言通常理解为不整体编译,由解释器解释一句执行一句。但JS不是直接对着代码解释执行,在解释执行之前,需要进行其他的步骤。
1.语法分析和语法分析,建立上下文关系的语法树;
1. var a = 1,b=2;
2. /*解析后的语法树
3. {
4. "type": "Program","body": [
5. {
6. "type": "VariableDeclaration","declarations": [
7. {
8. "type": "VariableDeclarator","id": {
9. "type": "Identifier","name": "a"
10. },"init": {
11. "type": "Literal","value": 1,"raw": "1"
12. }
13. },{
14. "type": "VariableDeclarator","name": "b"
15. },"value": 2,"raw": "2"
16. }
17. }
18. ],"kind": "var"
19. }
20. ]
21. }
22. */
json格式的语法树,每个对象就是一个节点(Node)。可以看到每个节点都有一个type属性用来标识当前节点的类型。
Program类型,是语法树的根节点,只有一个。它具有body属性包括所有的语法节点。然后整个的var a = 1,b=2;是一个变量声明语句(VariableDeclaration)。这个语句里面有一些变量声明(VariableDeclarator),这边就是a=1和b=2;变量声明符下面有两个属性,分别是id和init。也就是对应的左边的被赋值者和右边的值。Identifier与Literal都可以表示最小的单位,他们不再包含其他节点。
2.预编译
预编译有个特点:任何变量,如果未声明就赋值,那该变量为全局变量,即暗示全局变量(imply global)。并且所有的全局变量都是window的属性。
(1)产生GO对象
GO对象全称为 global object(全局对象,等同于window),在开始预编译时产生的对象,比AO对象先产生,用于存放全局变量,也称为全局作用域。
GO预编译三步骤
- 生成GO对象
- 将变量声明的变量名当做GO对象的属性名,值为undefinded
- 将声明函数的函数名当做GO对象的属性名,值为函数体
在javascript引擎在将语法检查后正确后生成的语法树复制进来之后,javascript引擎会对语法树当中的变量声明、函数声明以及函数的形参(var、function的变量提升在预编译阶段,var只声明,function声明并赋值)进行属性填充,这过程是发生在执行代码之前,也就是为真正的解析阶段做好准备,这既是为什么此阶段叫“预编译”的原因
<script>
test();//a,预编译阶段function声明并赋值,执行阶段可以运行
function test(){
console.log('a');
}
</script>
<script>
console.log(a);//undefined,预编译阶段var声明没有赋值赋值,执行阶段才给变量a赋值
var a = 123;
</script>
实战解析
<script>
//生成GO对象:GO{}
console.log(a);//function a () {},预编译阶段给a赋值函数
var a = 123;//执行阶段才给变量a赋值,执行前都是undefined
function a () {
}//预编译阶段已经声明并赋值,且函数没有立即执行,所以js解释执行阶段跳过此步
console.log(a); //执行阶段a被赋值为123
</script>
1、生成GO对象:GO{}
2、把var开头的声明变量添加进GO对象,不做赋值:
GO{
a:undefined;
}
3、将声明函数添加进GO对象,如果该变量已经被var声明过,则覆盖之前声明的变量,并赋值函数地址:
GO{
a:function a () {};//覆盖a的值
}
(2)函数的AO对象(GO预编译结束并开始执行代码,当遇到函数执行前进行函数预编译)
AO对象全称为:activation object (活跃对象/执行期上下文),当GO预编译完成之后,js开始执行,当遇到函数执行表达式(如:test())时,在函数执行前执行函数预编译,此时会产生一个AO对象,AO对象保存该函数的参数变量。
函数预编译步骤
1.产生AO对象
2.将函数的参数以及函数里面声明的变量当做AO对象的属性名,值全部为undefined。
3.将实参的值赋值给形参。
4.在函数里面声明的函数,函数名作为AO对象的属性名,值赋值给函数体。(若参数名和函数名重叠,则函数体值会覆盖参数值)
实战解析
<script>
function test(a) {
//形参和var声明提前,并给形参赋值
//function声明,如有相同变量,和覆盖
console.log(a);//function(){}
var a = 2;//赋值给a,则AO对象中a的值被覆盖为2
console.log(a);//2
function a () {}//预编译阶段覆盖形参a,被赋值为函数,执行阶段跳过
console.log(a);//2
var b = function () {};//执行阶段b被赋值
console.log(b);//function(){}
function d(){}
}
test(1);
</script>
1、创建AO对象
AO{
//此时AO对象为空
}
2、确定AO对象的属性名
AO{
a:undefined; //函数参数
b:undefined; //函数里面声明的变量
}
3、将实参值赋值给形参
AO{
a:1; //函数参数
b:undefined; //函数里面声明的变量
}
4、处理函数里面的声明函数
AO{
a: function a () {} //变量名一样,值覆盖
b:undefined; //函数里面声明的变量
d:function d () {};
}
实战深入理解AO、GO
console.log(a);//undefined
a = 100;
function test() {
console.log(a);//优先找到函数作用域中声明的a,如找不到,往上层找
a = 200;
console.log(a);//函数内a被赋值200
var a = b = 300;//函数内a被赋值300,b没有声明,是全局变量,赋值为300
}
test();
console.log(b);//全局变量,300
var a;
console.log(a);//100,未操作全局变量a
1、生成GO对象:GO{}
2、将var声明变量添加进GO对象内,值为undefined
GO{
a:undefined;
}
3、将function声明函数添加进GO对象呢,值为函数体
GO{
a:undefined;
function test() { … };
}
4、预编译完成,执行代码:输出a,此时a的值为:undefined
5、a赋值,此时GO对象内的a值变为100
GO{
a:100;
function test() { … };
}
6、执行函数test(),生成AO对象:AO{}
7、将var声明的变量添加进AO对象,值为undefined
AO{
a:undefined;
}
8、函数没有传递参数,跳过函数预编译的第三步
9、函数里面没有声明函数,跳过函数预编译的第四步
10、执行函数,打印a,此时AO里面有属性a,则输出AO里面a的值,即输出: undefined
11、AO的a赋值200,此时AO的a的值为200
AO{
a:200;
}
12、将300的值赋值给b再赋值给a,此时b未声明,所以b为全局变量,将b添加进GO对象内,将a的值改为300
GO{
a:100;
function test() { … };
b:300;
}
AO{
a:300;
}
3.解释执行
在解释执行阶段,遇到变量需要解析时,会首先从当前执行环境的活动对象中查找,如果没有找到而且该执行环境的拥有者有prototype属性时则会从prototype链中查找,否则将会按照作用域链查找。遇到var a = ...这样的语句时会给相应的变量进行赋值(注意:变量的赋值是在解释执行阶段完成的,如果在这之前使用变量,它的值会是undefined
网友评论