上篇介绍完 JS 的作用域,那接下来的几篇就要讲讲跟作用域相关的内容了。

1、执行上下文

执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。

执行上下文类型

阅文前端团队翻译的《理解 Javascript 执行上下文和执行栈》中所说:

  1. 全局执行上下文:这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
  2. 函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论。
  3. eval 函数执行上下文:运行在 eval 函数中的代码也获得了自己的执行上下文。

2、执行上下文栈

执行栈,在其他编程语言中也被叫做调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文,这与堆栈中栈的原理类似。

当 JS 引擎开始编译 JS 代码时,会创建一个全局执行上下文压入到执行上下文栈的最顶层,当调用一个函数时,就会创建一个函数执行上下文压入执行上下文栈的最顶层。

当最新的函数执行完毕后,其执行上下文将会从执行上下文栈弹出,然后将执行下一个函数。

我们可以将执行上下文栈模拟为一个空数组:

1
stack = [];

通过下个例子来理解一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = 1;
function foo1() {
console.log(a);
}

function foo2() {
foo1();
}

function foo3() {
foo2();
}

foo3();

当执行上述例子时,先将全局执行上下文推入执行上下文栈

1
stack = [globalContext];

当函数调用的时候开始执行函数上下文:

1
2
3
4
5
6
7
8
9
10
11
12
stack.push(<foo3>, functionContext);
// foo3函数中调用了foo2
stack.push(<foo2>, functionContext);
// foo2函数中调用了foo1
stack.push(<foo1>, functionContext);
// foo1执行完毕
stack.pop();
// foo2执行完毕
stack.pop();
// foo1执行完毕
stack.pop();
// 之后再执行新的代码,但底层永远留有一个globalContext

3、执行上下文的创建

在执行上下文创建的时候,分为两个阶段:1、创建阶段 2、执行阶段

3.1、创建阶段

创建阶段有三个重要的属性:

  1. this
  2. 词法环境(作用域链)
  3. 变量环境

3.1.1、this

在全局上下文中,this 指向的是全局对象,在浏览器中,指向的是 window
在函数上下文中,this 指向的是函数的调用方,如果是对象调用该函数,则指向的是这个对象,否则 this 指向的是全局对象或者 undefined(严格模式)

1
2
3
4
5
6
7
8
9
10
11
12
let person = {
name: "Jack",
eat: function () {
console.log(this.name);
},
};
person.eat(); // 此时的this指向person,因为是person调用的该函数

function foo() {
console.log(this);
}
foo(); // 此时的this指向window

3.1.2、词法环境(作用域链)

词法环境是一个包含标识符变量映射的结构。(这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用)

词法环境包括两个部分:

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

在全局上下文中,对外环境的引用为 null,它拥有一个全局的对象(windowd 对象)以及其关联的属性和方法和用户自定义的全局变量。

在函数上下文中,用户自定义的变量存储在环境记录中,对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。另外在函数上下文创建时,在其环境记录中还会生成一个 arguments 对象,该对象包含了索引和传递给函数的参数之间的映射以及传递给函数的参数的长度(数量)。如下所示:

1
2
3
4
5
6
function add(a, b) {
return a + b;
}
add(2, 3);
// arguments对象
Arguments: {0: 2, 1: 3, length: 2}

我们以一个例子,并用伪代码来看一下在词法环境中 JS 内部都做了什么:

1
2
3
4
5
6
var a,
b = 1;
function foo(a, b) {
return a + b;
}
foo(2, 3);

我们用 ER 代表环境记录,用 outer 代表对外部环境的引用,伪代码写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
全局上下文:
GlobalExectionContext = {
ER: {
a,
b,
foo
},
outer: null
}
当运行函数时,会产生函数上下文:
FunctionExectionContext = {
ER: {
arguments: {
0: 2,
1: 3,
length: 2
}
},
outer: <GlobalExectionContext>
}

在 ES8 中规定环境记录同样拥有两个类型:

  1. 声明性环境记录:存储变量、函数和参数。一个函数环境包含声明性环境记录。
  2. 对象环境记录:用于定义在全局执行上下文中出现的变量和函数的关联。全局环境包含对象环境记录。

3.1.3、变量环境

变量环境也是一个词法环境,所以他包含了词法环境的所有特性,在 ES6 中变量环境包括两个一个是 LexicalEnvironment 组件另一个是 VariableEnvironment 组件,他俩的区别在于前者用于存储函数声明和变量( let 和 const )绑定,而后者仅用于存储变量( var )绑定。

这样我们再举例来看一下 JS 内部究竟干了什么:

1
2
3
4
5
6
7
let a = 1;
const b = 2;
var c;
function add(a, b) {
return a + b;
}
c = add(1, 3);

我们同样用伪代码来模拟一下吗,用 LE 代表 LexicalEnvironment,用 VE 代表 VariableEnvironment:

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
全局上下文:
GlobalExectionContext = {
this: <Global Object>,
LE: {
ER: {
a: <uninitialized>,
b: <uninitialized>,
add: <func>
},
outer: null,
},
VE: {
ER: {
c: undefined
},
outer: null,
}
}
当调用add时,生成函数上下文:
FunctionExectionContext = {
this: <Global Object>,
LE: {
ER: {
arguments: {
0: 1,
1: 3,
length: 2
}
},
outer: <GlobalExectionContext>,
},
VE: {
ER: null,
outer: <GlobalExectionContext>,
}
}

此时你一定会对 let、const、var 的值有一个疑惑,那是因为在创建阶段的时候,代码会被扫描和解析成变量和函数声明,函数声明存储在环境中,而 var 会被定义为 undefined,let 和 const 会被定位为 uninitialized,这就是为什么你可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因,这个错误就是暂时性死区或者叫做临时死亡(TDZ),原因就是在访问一个已经声明但没有初始化的变量。

3.2、执行阶段

在此阶段,完成对所有变量的分配,最后执行代码。
注:在执行阶段,如果 Javascript 引擎在源代码中声明的实际位置找不到 let 变量的值,那么将为其分配 undefined 值。

这篇查阅了很多相关资料,但是觉得很多还是理解的不是很透彻,今后有时间会继续补充,也希望大家能多给些评价。

参考:
【译】理解 Javascript 执行上下文和执行栈