Fork me on GitHub

JavaScript 中的执行上下文、词法环境、变量环境

关于作用域请看另一篇文章:JavaScript 的作用域
关于this请看另一篇文章:JavaScript 中的 this

上篇文章我们讲了作用域,知道JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。现在我们聊聊执行上下文。

执行上下文(Execution Context)

执行上下文是当前JavaScript代码被引擎解析和执行时所在环境的抽象概念。帮助JavaScript引擎管理整个解析和运行代码的复杂过程。为我们的可执行代码块提供了执行前的必要准备工作,例如变量对象的定义、作用域链的扩展、提供调用者的对象引用等信息。
在代码执行过程中,可能会出现多个执行上下文,但运行的执行上下文最多只有一个。为了管理执行上下文,我们引入了执行上下文栈,处于栈顶的那个元素就是运行的执行上下文。当解释器遇到函数、块语句、try...catch时,都会创建一个新的执行上下文压入栈,成为当前上下文。

执行上下文的生命周期

创建阶段

  • 确定this
  • 创建作用域链
  • 创建变量、函数、参数:用当前函数的参数列表arguments初始化一个变量对象,并将当前执行上下文与之关联,函数代码块中声明的变量和函数将作为属性添加到这个变量对象上。在这一阶段,会进行变量和函数的初始化声明,变量统一定义为undefined需要等到赋值时才会有确值,而函数则会直接定义。

执行阶段
代码开始逐条执行,在这个阶段,引擎开始对定义的变量赋值,开始顺着作用域链访问变量,如果内部有函数调用就创建一个新的执行上下文压入执行栈并把控制权交出。

销毁阶段
函数执行完成后,当前执行上下文(局部环境)会被弹出执行上下文栈并且销毁,控制权被重新交给执行栈上一层的执行上下文。

执行上下文的种类

分为全局执行上下文和函数执行上下文:

  • 全局执行上下文Javascript引擎首次开始解析代码时创建。只有一个。
  • 函数执行上下文:当一个函数被调用时,会创建一个活动记录(执行上下文),这个纪录会包含函数在哪里被调用(调用栈)、调用的方式、传入的参数等信息。this就是这个纪录的一个属性,会在函数执行的过程中用到。

执行上下文的结构

包含词法环境(Lexical Environment)变量环境(Variable Environment)this的值。
An execution context has the following fields:
Environments: LexicalEnvironment and VariableEnvironment are what keep track of variables during runtime. Two references to environments. Both are usually the same.

  • LexicalEnvironment: resolve identifiers.(保存通过letconstwith()try-catch创建的变量)
  • VariableEnvironment: hold bindings made by variable declarations and function declarations.(保存通过var声明或function(){}声明的变量)
  • ThisBinding: the current value of this.

执行上下文的抽象

我们将执行上下文抽象成伪代码,如下:

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

词法环境和变量环境又是什么?

环境(Environments)

我们看看 ECMAScript 5 中对EnvironmentsExecution Contexts的解释:

Lexical environments hold variables and parameters. The currently active environment is managed via a stack of execution contexts (which grows and shrinks in sync with the call stack). Nested scopes are handled by chaining environments: each environment points to its outer environment (whose scope surrounds its scope). In order to enable lexical scoping, functions remember the scope (=environment) they were defined in. When a function is invoked, a new environment is created for it’s arguments and local variables. That environment’s outer environment is the function’s scope.

由此可知环境其实就是作用域。在规范中作用域更官方的叫法是词法环境词法环境作用域的内部实现机制。环境外部还有环境,形成链条,也就是作用域链
当函数被调用时,会创建新的执行上下文,其中包含:确定this指向和环境,相当于将作用域上下文进行关联,将标识符保存在环境记录中。执行时引擎会根据作用域规则,查找参数和变量等标识符,找到的话就保存在环境记录中。
随着执行栈的变化,随着执行函数的变化,当前的环境作用域也是变化的,只是环境是函数定义时确定的,this是函数执行时确定的。看一个例子:

1
2
3
4
5
6
7
8
function foo () {
console.log(color)
}
function bar () {
const color = 'yellow'
foo()
}
bar() // color is not defined

foo执行时,创建新的上下文并入栈,this指向window,词法环境无标识符,外部环境是全局环境。引擎在当前词法环境/作用域找不到color,沿着作用域链,外部环境是foo定义时的外部全局环境/全局作用域,而不是调用时的外部bar的作用域,对bar的作用域无访问权限,所以外部也找不到,就会报错。

词法环境(Lexical Environment)

词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。简单来说词法环境是一种持有标识符—变量映射的结构。这里的标识符指的是变量/函数的名字,而变量是对实际对象或原始数据的引用。

词法环境的种类

词法环境有三种类型:

  • 全局环境(Global Environment):是一个没有外部环境的词法环境,其外部环境引用为null。拥有一个全局对象(window对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this的值指向这个全局对象。
  • 函数环境(Function Environment):用户在函数中定义的变量被存储在环境记录中,包含了arguments对象。其外部环境可以是全局环境,也可以是包含内部函数的外部函数环境。
  • 模块环境(Module Environment):每个模块有自己的词法环境,存储了包括imports在内的所有的top-level declarations。其外部环境引用为全局环境。

avatar

词法环境的结构

avatar

词法环境由两部分组成:一个Environment Record,还有一个指向外层Lexical Environment的可空引用。

  • 环境记录(Environment Record):An environment record maps identifiers to value. that maps variable names to variable values. This is where JavaScript stores variables. One key-value entry in the environment record is called a binding. 环境记录就是存储当前环境下的标识符-变量key-value的映射,存储变量、函数声明的实际位置。它分为三类:
    • Declarative Environment Record:store the effects of variable declarations, and function declarations.
    • Object Environment Record:are used by the with statement and for the global environment. They turn an object into an environment. For with, that is the argument of the statement. For the global environment, that is the global object.
    • Global Environment Record
  • 对外部环境的引用:A reference to the outer environment (null in the global environment) - the environment representing the outer scope of the scope represented by the current environment. 可以访问其外部词法环境。

其中Declarative Environment Record又可分为两类:Function Environment Records函数环境记录和Module Environment Records模块环境记录。
avatar

词法环境的抽象

我们将词法环境(即作用域)抽象成伪代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GlobalExectionContext = {       // 全局执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Object", // 全局环境
outer: <null> // 对外部环境的引用
}
}
}

FunctionExectionContext = { // 函数执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Declarative", // 函数环境
outer: <Global or outer function environment reference> // 对外部环境的引用
}
}
}

变量环境(Variable Environment)

变量环境也是一个词法环境,variable environment is a certain type of lexical environment,因此它具有上面定义的词法环境的所有属性。
词法环境变量环境的区别在于前者用于存储函数声明和变量(let const)绑定,而后者仅用于存储变量(var)绑定。函数声明存储在变量环境中,而函数表达式存储在词法环境中。当遇到withcatch语句时,语句内部声明的变量是存储在外层词法环境中的,而外层的变量环境保持不变,当语句被销毁时,外层词法环境恢复。

例子:

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
// 全局执行上下文
GlobalExectionContext = {

ThisBinding: <Global Object>,

// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},

// 变量环境
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
c: 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>
}
}

附录

创建上下文时确定 this 的值(This Binding)

  • 全局执行上下文中,this 的值指向全局对象。
  • 函数执行上下文中,this 的值取决于函数的调用方式。具体有:默认绑定、隐式绑定、显式绑定、new 绑定、箭头函数。

Record 和 Field

ES6 中将键值对的数据结构称为Record,其中的每一组键值对称为field。这就是说,一个 Record由多个field组成,而每个field都包含一对key-value。可以将Record看做一个对象{}

Identifier Binding

标识符绑定,将一个标识符和对应的值(数字、函数、对象等)绑定在一起。通俗说就是将值赋值给标识符。

Identifier Resolver

标识符解析,指在运行的执行上下文中的词法环境里,通过标识符获得其对应绑定的过程。这一过程和原型链查找类似。通俗说就是获取标识符的值。