执行上下文

大半个月前,去重温了一下 冴羽 的博客。就想着把执行上下文的东西写写,但是因为考试,一直没有写出来,现在有空,刚好就把它给写完,作为 2018 年最后一篇技术文。

执行上下文栈

JavaScript 顺序执行下来的时候,会遇到三种可执行代码,全局代码函数代码eval代码

当 JavaScript 遇到执行到这些代码的时候,会创建一个环境,执行上下文,与此同时,会将这个环境压入一个名为执行上下文栈的环境去管理不同的执行上下文。

执行上下文(Execution context);

执行上下文栈(Execution context stack, ECS);

形象点可以模拟成一个数组 ECStack = [];

在 Javascript 开始解释代码的时候,最先遇到的是全局代码,所以全局代码的执行上下文最先入栈,globalContext.

1
2
3
4
5
const ECStack = [];
ECStack.push(globalContext);
ECStack = [globalContext];

当遇到需要执行一个函数时,生成执行上下文,会将其压入 ECStack 栈的栈底,当函数执行完毕的时候,将其弹出执行上下文栈

全局代码的执行上下文贯穿整份代码前后,所以不管怎样,只要 js 代码还没运行完,ECStack 中始终会有 globalContext.

执行上下文

每个执行上下文都有三个重要的属性

  • 变量对象 / 活动对象(variable object / activation object, VO / AO)
  • 作用域链(scope chain)
  • this

变量对象 (Variable object VO)

变量对象是与执行上下文相关的数据作用域,存储了上下文种定义的变量和函数声明

也就是说,变量对象其实是存了该环境下的变量和函数。

  • 全局上下文的变量对象就是全局对象

  • 函数上下文中用活动对象 (activation object, AO) 表示变量对象

变量对象是在进入执行上下文时创建的,所以变量对象在一开始执行代码的时候就被创建了,因为此时全局代码的执行上下文被创建了;但是活动对象,是在函数被调用时通过函数的 arguments 属性初始化,然后生成对应的执行上下文,随函数执行完毕而销毁,除了闭包这种情况。

函数的执行过程:分析和执行,可分为 进入执行上下文 代码执行

进入执行上下文

这时候是初始化 arguement,函数声明,变量声明

这个阶段中,函数声明优先变量声明,如果变量名称和函数名称相同,则被忽略,不处理名称相同的变量名称。

  • 如果函数体中没有用 var 去声明变量,这个变量不会进入函数执行上下文中
1
2
3
4
5
6
7
8
9
10
function foo(a){
var b = 2;
function c(){
console.log('hello')
};
var d = function(){
console.log('world')
};
e = 5;
}

1、调用这段代码时,先通过 arguements 初始化

1
2
3
4
5
6
AO = {
arguments: {
0: 1,
length: 1
}
};

2、进入执行上下文,添加形参、函数声明、变量声明,函数在此阶段会优先得到声明

1
2
3
4
5
6
7
8
9
10
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
代码执行

3、顺序执行,改变 AO 中变量对象的值

1
2
3
4
5
6
7
8
9
10
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}

作用域链(Scope chain)

查找变量的时候,现在当前上下文的变量对象中查找,如果没有,就往上一级找,最终一直找到全局上下文的变量对象,这就构成了作用域链

以函数的创建和激活两个时期来讲解作用域链是如何创建和变化的

创建

在函数创建时,会将所有父级的变量对象保存到函数的属性中,[[scope]]

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
function bar() {}
}
foo.[[scope]] = [
globalContext.VO
]
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
]

如上创建函数的作用域链

激活

当函数激活时,创建完活动对象时,会将当前活动对象加入作用域链的顶端

1
2
3
4
5
6
var scope = "global scope";
function checkscope() {
var scope2 = "local scope";
return scope2;
}
checkscope();

1、创建时,生成函数的[[scope]]属性

1
2
3
checkscope.[[scope]] = [
globalContext.VO
]

2、开始执行的时候,函数入执行上下文栈

1
ECStack = [checkscopeContext, globalContext];

3、准备开始之前,会将函数的[[scope]]复制创建函数执行上下文的作用域链

1
2
3
checkscopeContext = {
Scope:checkscope.[[scope]]
}

4、执行函数第一步,分析函数,进入执行上下文,生成活动对象

1
2
3
4
5
6
7
checkscopeContext = {
AO:{
arguments:{},
scope2:undefined
},
Scope:checkscope.[[scope]]
}

5、执行函数第二步,将活动对象压入 Scope 顶端

1
2
3
4
5
6
7
checkscopeContext = {
AO: {
arguments: {},
scope2: undefined
},
Scope: [AO, [[Scope]]]
};

6、执行函数第三步,代码执行,按代码顺序执行,改变 AO 中变量对象的值

1
2
3
4
5
6
7
checkscopeContext = {
AO: {
arguments: {},
scope2: "local scope"
},
Scope: [AO, [[Scope]]]
};

7、顺序执行完函数代码,函数上下文从执行上下文栈中弹出

1
ECStack = [globalContext];

至此,函数执行上下文的变量对象和作用域链已经创建完成,至于 this 嘛,有点麻烦,加之天色已经不早了,那就明年再写了吧。

  • 情不知所起,一往而深