JavaScript进阶笔记(一):执行上下文和执行栈

一、执行上下文

1.1 定义

执行上下文是 JS 代码解释和执行时所处的抽象环境。

1.2 种类

  • 全局执行上下文:只有一个,浏览器中指定是 window 对象,this 指向这个对象。
  • 函数执行上下文:无数个,每次函数被调用都会创建一个。
  • Eval函数执行上下文:不常用,很少见。

二、执行栈

执行栈,也叫调用栈,具有后进先出的特点,用来存储程序执行过程中创建的所有的执行上下文。

在程序初次执行时,首先创建全局执行上下文 Push 到栈中。之后每次发生调用 JS 引擎都会创建新的函数执行上下文并 Push 到执行栈的栈顶。当栈顶的函数执行完成后,相应的执行上下文会被 Pop 出栈,继续执行下一个执行上下文。

关于调用栈有五个点需要清楚:

  • 执行是单线程的。
  • 同步执行。
  • 只有一个全局上下文,永远在栈底。
  • 函数上下文,不唯一。
  • 每次调用函数都会创建新的上下文,包括调用自身。

三、执行上下文的创建

上下文的创建分为两个阶段:创建阶段执行阶段

3.1 创建阶段(指的是函数被调用,但是函数内部还没执行的阶段)

创建阶段:绑定 this,词法环境,变量环境。

1
2
3
4
5
ExecutionContext = {  
ThisBinding = <this value>, // 确定this
LexicalEnvironment = { ... }, // 词法环境
VariableEnvironment = { ... }, // 变量环境
}

this 绑定,在全局上下文中,this 指向全局对象,浏览器中 this 指向 window 对象,而在 node 中指向 module 对象。函数上下文中,this 指向取决于调用方式。具体有:默认绑定、隐式绑定、显式绑定(硬绑定)、new 绑定、箭头函数。

3.1.1 词法环境

  1. 环境记录:存储变量和函数声明的实际位置。
  2. 对外部环境的引用:访问外部词法环境。

对于全局环境,没有外部环境所以为 null。有一个全局对象 window 及其关联的方法和属性以及用户定义的变量。this 指向这个对象。对于函数环境,外部环境是全局环境或者包含内部函数的外部函数环境。存储用户定义的变量,包括 arguments 对象。

3.1.2 变量环境

变量环境是特殊的词法环境。词法环境用于存储函数声明和变量(let和const) 绑定,而变量环境仅存储变量(var) 绑定。

使用例子进行介绍

1
2
3
4
5
6
7
8
9
10
let a = 20;  
const b = 30;
var c;

function multiply(e, f) {
var g = 20;
return e * f * g;
}

c = multiply(20, 30);

执行上下文如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 全局执行上下文
GlobalExectionContext = {

ThisBinding: <Global Object>, // this 指向 window

LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Object", // 全局环境
// 标识符绑定在这里
a: < uninitialized >, // 未初始化状态
b: < uninitialized >,
multiply: < func > // 函数
}
outer: <null>
},

VariableEnvironment: { // 变量环境
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
c: undefined, // var 绑定,初始化为 undefined 。
}
outer: <null>
}
}
// 函数执行上下文
FunctionExectionContext = {

ThisBinding: <Global Object>,

LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 函数环境
// 标识符绑定在这里
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment> // 外部环境
},
// 变量环境
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}

变量提升:创建阶段,函数声明存储在环境中,var声明的变量被设置为 undefined,而 let 和 const 声明的变量设置为未初始化。所以可以在声明前访问 var 定义的变量,但不可以在声明前访问 let 和 const 定义的变量。

3.2 执行阶段

完成对变量的分配,并执行。

四、执行上下文的应用

4.1 变量提升和函数提升

变量提升的原理:var 声明的变量是存储在执行上下文的变量环境中,并且默认是 undefined 。

函数声明的优先级高于变量。同一作用域下存在多个同名函数声明,后面的会替换前面的函数声明。

4.2 测验

两段代码的不同点,输出结果相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 代码一
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f(); // <<<<<
}
checkscope(); // <<<<<

// 代码二
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f; // <<<<
}
checkscope()(); // <<<<<

答案是:调用栈不同

代码一的调用栈顺序:

1
2
3
4
Stack.push(<checkscope>, functionContext);
Stack.push(<f>, functionContext);
Stack.pop();
Stack.pop();

代码二的调用栈顺序:

1
2
3
4
Stack.push(<checkscop>, functionContext);
Stack.pop()
Stack.push(<f>, functionContext)
Stack.pop()

五、函数上下文

函数上下文中,用活动对象来(AO)来表示变量对象。也就说活动对象和变量对象是一回事,只是叫法和所处的位置不同。变量对象是 JavaScript 规范中的叫法,进入执行上下文变量被激活,此时称为活动对象(AO)。

活动对象在进入函数上下文时被创建,通过 Arguments 属性初始化。

5.1 执行过程

执行上下文分为两个阶段:进入执行上下文和代码执行。

进入和执行阶段都做了什么以及 AO 的状态。

5.1.1 进入阶段

进入阶段没有执行代码,此时的变量对象:

  1. 函数的所有形参:没有实参,属性值为 undefined。
  2. 函数声明
  3. 变量声明
1
2
3
4
5
6
7
8
9
function foo(a) {
var b = 2;
function c() {}
var d = function() {};

b = 3;
}

foo(1);

此时的AO:

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
}

形参 arguments 已经赋值,但是变量还只是 undefined 。

5.1.2 执行阶段

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

总结如下:

  1. 全局上下文的变量对象初始化是全局对象
  2. 函数上下文的变量对象初始化只包括 Arguments 对象
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
  4. 在代码执行阶段,会再次修改变量对象的属性值

参考

木易杨前端进阶