在上一篇文章中,对变量声明和函数声明的定义提升进行了一个简要的说明,这里对其后面的运行机制再进行深一步的讨论。
其实定义提升是执行上下文代码的过程的一个很自然的结果。
执行上下文(Execute Context)
每次当控制器转到ECMAScript可执行代码的时候,就会进入到一个执行上下文(EC)。执行上下文按类型可以分为全局执行上下文和函数执行上下文。执行上下文在逻辑上组成一个堆栈。堆栈底部永远都是全局执行上下文(global execute context),而顶部就是当前(活动的)执行上下文。堆栈在EC进入和退出的时候被修改(进栈或出栈)。此外,每一次函数调用(也包括递归调用和使用Function构造函数)或者使用eval()方法时,都会创建一个新的EC,并入栈。
变量对象(Variable Object)
变量对象(缩写为VO)是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容:
变量 (var, 变量声明);
函数声明 (不包括函数表达式);
函数的形参。
我们可以将执行上下文中声明的这三类东西当作VO的属性,在全局执行上下文中,VO就是全局对象window自身,在函数执行上下文中,VO由活动对象(AO)代替。VO和AO区别不大,只是前者不能直接访问后者可以直接访问,可以当成一个东西(据说这是ECMA3里的老词,ECMA5没有提及了)。至此,EC的结构大致如下:
EC={
VO/AO:{
变量声明;
函数声明;
函数参数;
}
...
}
上下文代码执行
上下文代码的执行分为两个阶段
1、进入执行上下文
在这个阶段会初始化VO/AO对象。例如:
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
}
test(10); // call
当调用test(10)时,进入test的函数执行上下文,此时AO对象初始化如下:
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <指向函数"d">
e: undefined
};
2、执行代码
在这个阶段真正执行代码,并更新VO/AO的相关属性。例如运行到var c = 10时,c被更新为10,运行到var e = function _e(){}时,e指向了函数_e。
至此,我们应该明白了为什么会出现定义提升了。原来在执行代码前,也就是进入执行上下文时,就将声明的变量、函数和形参添加给了EC的VO/AO对象。这样在正式执行代码时,这些变量都是存在的并可以访问的。
从上面的论述来看,当执行代码时,遇到一个变量,可以在当前EC的VO/AO中进行查找。事实上确实是如此,只是有一点不同。EC有一个专门用于标识符解析的属性,叫作用域(scope)。于是EC的结构又变为如下形式:
EC={
VO/AO:{
变量声明;
函数声明;
函数参数;
}
scope:VO/AO+[[Scope]]
}
其中VO/AO就是当前EC的变量对象,[[Scope]]是所有父VO/AO的层级链,[[Scope]]是被调用函数的一个内部属性,通过它就可以进入到上层的VO/AO中。scope类似于一个数组,当前EC的变量对象总是被添加到[[Scope]]的最前面,这样scope就像一个链,被称为作用域链。如下面所示:
scope=[//作用域链
当前变量对象,
父变量对象层级链
]
作用域链专门用于标识符解析,标示符解析是一个处理过程,用来确定一个变量(或函数声明)属于哪个变量对象。标识符解析过程包含与变量名对应属性的查找,即作用域链中变量对象的连续查找,从最深的上下文开始,绕过作用域链直到最上层。这样一来,在向上查找中,一个上下文中的局部变量较之于父作用域的变量拥有较高的优先级。万一两个变量有相同的名称但来自不同的作用域,那么最深作用域起作用。
标识符解析的过程又与函数生命周期有关。
函数声明周期
函数生命周期分为声明和调用两个阶段。
声明阶段
在函数的声明阶段,函数声明会写入到执行环境的VO/AO中,而[[Scope]]是所有父级VO/AO的层级链,在函数声明阶段就存在,而且是静态存储,不会再改变。这里的父子关系由函数声明时的嵌套关系确定,而不是调用时确定。因为此时还没调用函数。
调用阶段
函数调用时,进入到函数执行上下文中,此时VO/AO创建并初始化,并被添加到[[scope]]的最前面,作用域链也随之确定。
下面用两个例子进行说明:
例子1:
var x = 10;
function foo() {
alert(x);
}
function test() {
var x = 20;
foo(); // 10
alert(x);//20
}
test();
分析:
调用test()时,进入到test函数的执行上下文,EC(test)={
AO:{
x: undefined
},
scope:AO+[[Scope]]([[Scope]]=全局变量对象(Global VO))
}
调用foo()时,进入到foo函数的执行上下文,EC(foo)={
AO:{},
scope:AO+[[Scope]]([[Scope]]=全局变量对象(Global VO))
}
foo弹框时需要查找x时,在foo的AO中找不到,于是去[[Scope]]中查找,x=10,所以弹出10。foo返回后,EC出栈,进入到test的EC中,弹出20。
例子2:
var x = 10;
function test() {
var x = 20;
foo(); //20
function foo() {
alert(x);
}
alert(x);//20
}
test();
分析:
调用test()时,进入到test函数的执行上下文,EC(test)={
AO:{
x: undefined,
foo:指向函数'foo'
},
scope:AO+[[Scope]]([[Scope]]=全局变量对象(Global VO))
}
调用foo()时,进入到foo函数的执行上下文,EC(foo)={
AO:{},
scope:AO+[[Scope]]([[Scope]]=EC(test).VO)
}
foo弹框时需要查找x时,在foo的AO中找不到,于是去[[Scope]]中查找,x=20,所以弹出10。foo返回后,EC出栈,进入到test的EC中,弹出20。
总结
参考文章:
1、执行上下文
2、变量对象
3、作用域链
4、js作用域