执行上下文
js在执行可执行代码时,会创建对应的执行上下文。在js引擎中用执行上下文栈(**Execution context stack,ECS**)来管理执行上下文。通俗来说,执行上下文就是执行代码的准备过程。
执行上下文栈
js最先解析的是全局代码,所以初始化时会将全局执行上下午压入ECS内,并且只有当整个应用程序结束时ECS才会被清空,所以在程序结束前,ECS底部始终有个全局执行上下文(globalContext)。
当执行一个函数时,就会创建一个函数执行上下文,并且压入ECS中,当函数执行完毕,就会将此函数执行上下文从栈中弹出。
每个执行上下文,都有三个重要属性
- 变量对象(Variable object ,
VO
) - 作用域链(Scope chain)
- this
1)变量对象
变量对象是与执⾏上下⽂相关的数据作⽤域,存储了在上下⽂中定义的变量和函数声明
全局上下⽂中全局对象
初始化时是全局对象;
可以通过this引用,在客户端环境中,全局对象就是window;
是由 Object 构造函数实例化的⼀个对象;
预定义的属性是可⽤;(比如可以直接调用Math.random())
作为全局变量的宿主;
函数上下⽂中活动对象(AO
)
函数上下⽂的变量对象初始化只包括 Arguments 对象;
只有当进入到一个执行上下文中,这个执行上下文的变量才会被激活。而只有被激活的变量对象上的各种属性才能被访问;
活动对象是在进⼊函数上下⽂时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象;
在进⼊执⾏上下⽂时会给变量对象添加形参、函数声明、变量声明等初始的属性值;
在代码执⾏阶段,会再次修改变量对象的属性值;
Tip:在进⼊执⾏上下⽂时,⾸先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明
的形式参数或函数相同,则变量声明不会⼲扰已经存在的这类属性。
执行过程
执⾏上下⽂的代码会分成两个阶段进⾏处理:进⼊执⾏上下⽂和执⾏
- 进⼊执⾏上下⽂
变量对象包括
1) 函数的所有形参
2) 函数声明
3) 变量声明
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
- 代码执⾏
在代码执⾏阶段,会顺序执⾏代码,根据代码,修改变量对象的值
执行完代码后,上面的例子会变成
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
2)作⽤域链
当查找变量的时候,会先从当前上下⽂的变量对象中查找,如果没有找到,就会从⽗级(词法
层⾯上的⽗级)执⾏上下⽂的变量对象中查找,⼀直找到全局上下⽂的变量对象,也就是全局对象。
这样构成的链表就叫作用域链。
函数创建
这是因为函数有⼀个内部属性 [[scope]],当函数创建的时候,就会保存所有⽗变量对象到其中,可以理解为 [[scope]] 就是所有⽗变量对象的层级链,举例
function father() {
function son() {
...
}
}
//函数创建时,各⾃的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
]
函数激活
当函数激活时,进⼊函数上下⽂,创建 VO/AO 后,就会将活动对象添加到作⽤链的前端。这时候执⾏上下⽂的作⽤域链,我们命名为 Scope
Scope = [AO].concat([[Scope]]);
这就是完整的作用域链
举个例子
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
这个函数的执行过程如下
- checkscope 函数被创建,保存作⽤域链到 内部属性[[scope]]
checkscope.[[scope]] = [
globalContext.VO
];
- 执⾏ checkscope 函数,创建 checkscope 函数执⾏上下⽂,checkscope 函数执⾏上下⽂被压⼊执
⾏上下⽂栈
ECStack = [
checkscopeContext,
globalContext
];
- checkscope 函数并不⽴刻执⾏,开始做准备⼯作,第⼀步:复制函数[[scope]]属性创建作⽤域链
checkscopeContext = {
Scope: checkscope.[[scope]],
}
- 第⼆步:⽤ arguments 创建活动对象,随后初始化活动对象,加⼊形参、函数声明、变量声明
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
- 第三步:将活动对象压⼊ checkscope 作⽤域链顶端
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
- 准备⼯作做完,开始执⾏函数,随着函数的执⾏,修改 AO 的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
- 查找到 scope2 的值,返回后函数执⾏完毕,函数上下⽂从执⾏上下⽂栈中弹出
ECStack = [
globalContext
]
3)this
ECMAScript 的类型分为语⾔类型和规范类型。主要介绍规范类型meta-values中的Reference
Reference
⽤来解释诸如 delete、typeof 以及赋值等操作⾏为的,它是只存在于规范⾥的抽象类型,由三部分组成。
- base value;(属性所在的对象或者 EnvironmentRecord)
- referenced name;(属性的名称)
- strict reference;
可以通过GetBase或IsPropertyReference来获取Reference 组成部分- GetBase 返回对象属性真正的值
var foo = 1; var fooReference = { base: EnvironmentRecord, name: 'foo', strict: false }; GetValue(fooReference) // 1;
- IsPropertyReference 如果 base value 是⼀个对象,就返回true。
如何确定当前this的值
- 计算 MemberExpression 的结果赋值给 ref
- 判断 ref 是不是⼀个 Reference 类型
a. 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为
GetBase(ref)
b. 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为
ImplicitThisValue(ref)
c. 如果 ref 不是 Reference,那么 this 的值为 undefined;