执行上下文 EC
JS控制器转到可执行代码的时候就会进入到可执行上下文(EC)。EC是抽象的概念,需要和可执行代码(executable code)概念进行区分。活动的执行上下文会组成一个堆栈。堆栈的底部是全局上下文,顶部就是当前活动的执行上下文。进入上下文,上下文被推入堆栈。离开上下文,上下文弹出堆栈。
全局上下文
初始的上下文堆栈, 全局上下文不包含任何function代码。
# 执行上下文堆栈
ECStack = [
globalContext,
]
函数代码
当进入function的时候,ECStack都会被推入新的元素(函数的执行上下文),但是不包括还没有执行的嵌套函数的上下文。return的时候或者异常没有catch的时候,ECStack顶部的执行上下文会被弹出。当所有代码执行完成后,ECStack中只会存在一个全局执行上下文。
function foo (trigger) {
if (trigger) {
return
}
// 递归调用
foo(true);
}
foo()
# 第一次调用foo函数
ECStack = [
<foo>functionContext,
globalContext,
]
# 递归第二次调用foo函数
ECStack = [
<foo>functionContext - recursion,
<foo>functionContext,
globalContext,
]
# 递归的函数return后
ECStack.pop()
# foo函数执行完成后
ECStack.pop()
# 此是ECStack
ECStack = [
globalContext,
]
调用上下文
调用上下文是eval函数调用时产生的上下文(calling context)。在汤姆大叔博客说,在firefox中,eval中实现了第二个参数,可以用于传递上下文,在指定的上下文中执行eval。但是我在Chrome中,实验了一下,Chrome没有实现。
eval('var x = 10');
(function foo() {
eval('var y = 20');
})();
// 上述代码EC的变化过程
ECStack = [
globalContext
];
ECStack = [
evalContext,
callingContext: globalContext
];
ECStack.pop();
ECStack = [
callingContext: <foo> functionContext,
globalContext
];
ECStack = [
evalContext,
callingContext: <foo> functionContext,
globalContext
];
ECStack.pop();
ECStack.pop();
ECStack = [
globalContext
];
变量对象/活动对象
变量对象VO, 是一个与执行上下文相关的特殊属性,它存储着上下文中以下内容:
- 变量
- 函数的声明(不包含函数表达式)
- 函数的形参
// VO,是执行上下文的属性
activeExecutionContext = {
VO: {
}
}
全局上下文的VO,可以通过VO的属性名称直接进行访问。全局对象自身就是VO。在其他上下文是无法直接访问VO对象的
var a = 10;
function foo(x) {
var b = 20;
};
foo(30);
# 上下文堆栈和VO对象
ECStack = [
# foo的上下文
functionContext: {
VO: {
x: 30,
b: 20,
}
}
# 全局上下文
globalContext: {
VO: {
a: 10
foo: <reference to function>
}
}
]
全局上下文变量对象
全局对象是在进入任何上下文之前就已经创建的对象,全局对象只存在一份。全局对象在创建阶段,会将Math、String、Date、parseInt作为自身属性,进行初始化。其他的全局变量也会当作全局对象的属性。全局上下文的VO,就是全局对象自己 globalContext.VO === global
// 全局对象
global: {
Math,
Date,
window: global // 引用自身
}
函数上下文的变量对象
函数执行上下文中VO无法直接访问。在函数的执行上下文中使用活动对象AO替代变量对象VO的功能。
AO是在进入函数执行上下文时被创建的,通过arguments属性初始化,arguments属性的值Arguments对象。Arguments对象的length属性是实参的个数,callee是当前函数的引用。
AO = {
arguments: Arguments
};
处理上下文代码的2个阶段
通过这节,我们可以了解到为什么在js中变量的值,默认是undefined。
执行上下文中代码分为两个阶段进行处理:
1. 进入执行上下文 2. 执行代码
阶段:进入执行上下文
在代码进入执行上下文时,还没有开始执行时,VO/AO就已经包含了下面的属性
- 形参, 形参会作为AO对象的属性,如果传参了属性会被赋值,否则是undefined。
- 函数声明,函数名和函数的内容作为变量对象的key和value(函数声明的优先级会高于变量声明,如果变量声明与函数声明有相同属性,函数声明会替代变量声明)
- 变量声明,如果变量声明和形参和函数声明的属性相同,变量声明不会干扰。(变量的声明在形参和函数声明之后。)
function Foo (a, b) {
var c = 10;
var d = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
Foo(10);
此时的AO对象如下。AO对象不包含x函数,x函数不是函数声明,而是函数表达式。x不存在VO/AO中。未保存的函数表达式,只能在自己的执行上下文中被调用。由于函数声明优先,所以在进入执行上下文的阶段VO变量对象的d属性目前是函数,而不是数字。
// 进入执行上下文但是还没有执行的代码的活动对象
AO = {
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">,
e: undefined
}
阶段:执行上下文中的代码
接下来进入代码执行阶段,此时AO/VO对象都已经有了属性,但是属性值还是undefined,代码执行阶段VO属性会被更新赋值。
// 代码执行阶段,AO活动对象的值被更新。
AO = {
a: 10,
b: undefined,
c: 10,
d: 10, // 代码在执行阶段,属性d会被赋值为10
e: undefined
}
所以为什么js中变量的默认值是undefined的?因为在进入执行上下文的阶段,VO/AO对象的属性值就被赋予了undefined,所以js变量值默认是被赋予了undefined,而不是天生就是undefined。
所以下面的代码,即使永远走不到b的分支,b也会赋值为undefined,这是因为在进入执行上下文的阶段放入VO中被赋值为undefined。
if (true) {
var a = 1;
} else {
var b = 2;
}
alert(a); // 1
alert(b); // undefined
作用域链
在JS中允许嵌套函数,每一个函数都有自身的变量对象VO,由于函数无法直接访问VO,对于函数来说就是AO活动对象。而作用域链则是AO的列表。作用域链作用是用于变量的查询。
函数的执行上下文作用域链,在函数调用时创建,包含了VO/AO属性,以及[[scope]]
属性。而作用域链等于VO + [[scope]]属性
ECStack = [
functionContext: {
VO: {},
this: thisValue,
[[scope]]: [...],
Scope: [...] // 作用域链( VO + [[scope]])
}
]
[[scope]]
属性
[[scope]]
是挡墙执行上下文的属性,[[scope]]
属性包含所有父变量对象(VO)的层级链。[[scope]]
在函数创建时被创建并存储,不可变。直到函数被销毁,也就是说作用域链在定义函数时就已经确定的,即使函数不被调用,它的的[[scope]]
属性也已经被写入了。
function foo () {}
// foo函数的`[[scope]]`属性
// 包含了父变量对象,比如全局对象的VO
fooContext.[[scope]] = [
globalContext.VO
]
函数激活
进入上下文,创建AO/VO对象后,AO/VO对象会添加Scope属性的第一位,这对于标示符解析很重要,因为变量名的查找是从最深处开始的,由于当前的AO/VO对象会放到第一位,所以局部的变量优先级在查找时会高于父作用域的变量。
Scope属性 = [当前执行上下文的AO/VO, [[scope]]]
下面是一个例子
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
}
bar();
}
foo();
上面函数的执行上下文堆栈如下
ECStack = [
<bar>functionContext: {
[[scope]]: [
<foo>functionContext.VO
globalContext.VO
],
VO: {
z: 30
},
Scope: [
<bar>functionContext.VO,
<foo>functionContext.VO,
globalContext.VO
]
},
<foo>functionContext: {
[[scope]]: [
globalContext.VO
],
VO: {
y: 20,
bar: <reference to function>
}
Scope: [
<foo>functionContext.VO,
globalContext.VO
]
},
globalContext: {
VO: {
x: 10
foo: <reference to function>
},
},
]
eval与作用域链
代码eval的上下文与当前的调用上下文(calling context)拥有同样的作用域链
代码执行对作用域链的影响
在代码执行阶段with和catch会修改作用域链,它们会被添加到作用域的最前端
当遇到with或者catch时,Scope = [AO|VO, [[Scope]]] ==> [withObject|catchObject, AO|VO, [[Scope]]]
var foo = {x: 10, y: 20};
with (foo) {
alert(x); // 10
alert(y); // 20
}
被修改的作用域链
Scope = [foo, AO|VO, [[Scope]]]
var x = 10, y = 10, z = 10;
with ({x: 20, z: 20}) {
// 这里修改的是with增加的x
// 但是y修改的是外面的y
var x = 30;
var y = 30;
// 这里访问的with增强的x和z
alert(x); // 30
alert(y); // 30
alert(z); // 20
}
//
alert(x); // 10
alert(y); // 30
alert(z); // 10
在with执行完成后,它的特定对象从作用域链中移除(已改变的变量x,z也从那个对象中移除),即作用域链的结构恢复到with得到加强以前的状态。所以x,z变为了10.
catch语句也会创建一个新属性的对象,属性名是异常参数名
try {
...
} catch (ex) {
alert(ex);
}
var catchObject = {
ex: <exception object>
};
Scope = catchObject + AO|VO + [[Scope]]
闭包
闭包是代码块和创建该代码块的上下文中数据的结合
函数的父级上下文数据保存在函数的内部属性[[scope]]
中,由于js是使用静态词法作用域的语言,包含了父级上下文的数据的[[scope]]
属性,在函数创建时就已经保存了,不管函数有没有调用。
因为作用域链,在js中所有函数都是闭包,因为它们在创建的时候,都保存了上层上下文的作用域链。
var x = 10;
function foo() {
alert(x);
}
// foo就是一个闭包
foo: <FunctionObject> = {
[[Call]]: <code block of foo>,
[[Scope]]: [
global: {
x: 10 // foo的[[scope]],包含了父变量对象
}
],
};
引用同一个[[scope]]
在同一个上下文中创建的闭包,共用一个[[scope]]
属性,对[[scope]]
的修改,会影响到其他闭包。也就是说同一个上下文创建的闭包,共享同一个父作用域。
var foo;
var bar;
function test() {
var x = 1;
foo = function () { return ++x; };
bar = function () { return --x; };
x = 2;
// 3
alert(firstClosure());
}
test();
alert(foo()); // 4
alert(bar()); // 3
一个经典的例子,下面的例子中打印都是3,因为k是同一个父作用域的k
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = function () {
alert(k);
};
}
data[0](); // 3
data[1](); // 3
data[2](); // 3
改造下, 打印的结果是0,1,2,因为IFEE返回的函数表达式打印的是_helper函数的活动对象的属性x。但是如果打印的是k,结果还是3。
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = (function _helper(x) {
return function () {
alert(x);
};
})(k);
}
data[0](); // 0
data[1](); // 1
data[2](); // 2