什么是执行上下文
当浏览器的解释器开始执行我们的js代码的时候,js代码运行所处的环境可以被认为是代码的执行上下文,执行上下文(简称-EC)是ECMA-262标准里的一个抽象概念,用于同可执行代码(executable code)概念进行区分。一般来讲,执行上下文可以在以下三种情况产生:
1. 全局上下文(globalContext) 2. function 内部 3. Eval code.
看个例子,包含全局和function内部上下文
紫色框内表示全局的执行上下文,同时内部会有3个不同的Function context, function context可以有多个,但是全局上下文只有一个,并且当解释器开始执行代码的时候就会创建全局上下文并进入。 我们可以创建任意多个Function context,声明一个方法并执行的时候会自动创建该function context,同时创建一块区域,在该区域内创建的变量或其他声明不直接被外部context所访问。
执行上下文堆栈
浏览器内部js解释器是按照单线程的方式实现,意味着内部只能同时在做一件事情,其他的调用都会在被称为执行堆栈的地方排队。看下面的图:
当浏览器加载js的时候,就会默认进入全局上下文,如果全局代码中开始执行function,则会创建一个新的execution context,并把该execution context push到栈顶。
如果在function里面又调用内部function,则会执行相同的操作,创建新的execution context,并push到栈顶。看例子:
(function foo(i){ if(i === 3){ return; }else{ foo(++i); } } )(0)
foo会执行三次,每次执行会生成新的execution context,执行结束则自动出栈。
执行上下文的细节
现在我们知道伴随着function的调用,都会产生一个新的context,在解释器内部,大致分为两个阶段:
Stage I:创建阶段(function被调用,但是在开始执行任何代码之前)
创建阶段大致做了以下几件事情:
①:生成变量对象。该阶段把所有的声明都以key-value的形式提取出来,包括 函数的形参(value为实参的值),function arguments,内部function名(value是对内部function的引用),function内的变量声明(value值统一为undefined)。
②:创建作用域链。作用域链包含该上下文中的变量对象和所有父上下文的变量对象,用于变量查找)
③:给this赋值。关于this的理解,参考《对javascript this的理解》
Stage II:执行阶段(给变量赋值,逐步执行)
理解了以上两个执行阶段后,我们可以大体描绘一下执行上下文中有哪些东西,可以用一个带有3个属性的对象来表示:
executionContextObj = { scopeChain: { /* variableObject + all parent execution context's variableObject */ }, variableObject: { /* function arguments / parameters, inner variable and function declarations */ }, this: {} }
对象变量和活动对象 Variable/Activation Object[VO/AO]
executionContextObj在function每次被调用的时候创建,在上文提到的StageI阶段,解释器会对function进行扫描,包括function的arguments, 参数,内部变量声明和内部function声明,扫描的结果会被放到 我们称为 对象变量的对象中(variable object).
看例子:
var a = 10; function test(x) { var b = 20; }; test(30);
以上代码对应的变量对象应该为:
VO(global context) = { a: 10, test: <reference to function> } VO(test function context) = { x:30, b:20 }
具体分为两种,
全局上下文中的对象变量
首先,我们要给全局对象一个明确的定义:
全局对象(Global object) 是在进入任何执行上下文之前就已经创建了的对象;
这个对象只存在一份,它的属性在程序中任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。
全局对象初始创建阶段将Math、String、Date、parseInt作为自身属性,等属性初始化,同样也可以有额外创建的其它对象作为属性(其可以指向到全局对象自身)。例如,在DOM中,全局对象的window属性就可以引用全局对象自身(当然,并不是所有的具体实现都是这样):
global = {
Math: <...>,
String: <...>
...
...
window: global //引用自身
};
当访问全局对象的属性时通常会忽略掉前缀,这是因为全局对象是不能通过名称直接访问的。不过我们依然可以通过全局上下文的this来访问全局对象,同样也可以递归引用自身。例如,DOM中的window。综上所述,代码可以简写为:
String(10); // 就是global.String(10);
// 带有前缀
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;
因此,回到全局上下文中的变量对象——在这里,变量对象就是全局对象自己:
VO(globalContext) === global;
非常有必要要理解上述结论,基于这个原理,在全局上下文中声明的对应,我们才可以间接通过全局对象的属性来访问它(例如,事先不知道变量名称)。
var a = new String('test');
alert(a); // 直接访问,在VO(globalContext)里找到:"test"
alert(window['a']); // 间接通过global访问:global === VO(globalContext): "test"
alert(a === this.a); // true
var aKey = 'a';
alert(window[aKey]); // 间接通过动态属性名称访问:"test"
函数上下文中的变量对象
在函数执行上下文中,VO是不能直接访问的,此时由活动对象(activation object,缩写为AO)扮演VO的角色。
VO(functionContext) === AO;
活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。arguments属性的值是Arguments对象:
AO = {
arguments: <ArgO>
};
Arguments对象是活动对象的一个属性,它包括如下属性:
- callee — 指向当前函数的引用
- length — 真正传递的参数个数
- properties-indexes (字符串类型的整数) 属性的值就是函数的参数值(按参数列表从左到右排列)。 properties-indexes内部元素的个数等于arguments.length. properties-indexes 的值和实际传递进来的参数之间是共享的。
例如:
function foo(x, y, z) {
// 声明的函数参数数量arguments (x, y, z)
alert(foo.length); // 3
// 真正传进来的参数个数(only x, y)
alert(arguments.length); // 2
// 参数的callee是函数自身
alert(arguments.callee === foo); // true
// 参数共享
alert(x === arguments[0]); // true
alert(x); // 10
arguments[0] = 20;
alert(x); // 20
x = 30;
alert(arguments[0]); // 30
// 不过,没有传进来的参数z,和参数的第3个索引值是不共享的
z = 40;
alert(arguments[2]); // undefined
arguments[2] = 50;
alert(z); // 40
}
foo(10, 20);
综上,来看一个比较综合的例子:
function foo(i) { var a = 'hello'; var b = function privateB() { }; function c() { } } foo(22);
foo被调用,StageI阶段:
foo executionContextObj = { scopeChain: { ... }, varaible object :{ arguments:{ length:1, callee : foo,//对调用foo的引用 0:22 } i : 22, c : <reference function()>, a : undefined, b : undefined }, this : global }
Stage II :
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, callee:foo length: 1 }, i: 22, c: pointer to function c() a: 'hello', b: pointer to function privateB() }, this: global }