简单地讲,可以分为两个阶段:编译阶段、执行阶段。
# 编译阶段
# 词法分析 (Scanner)
将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元 (token)。
[
{"type": "Keyword","value": "var"},
{"type": "Identifier","value": "name"},
{"type": "Punctuator","value": "="},
{"type": "String","value": "'finget'"},
{"type": "Punctuator","value": ";"}
]
2
3
4
5
6
7
# 语法分析 (Parser)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "name"
},
"init": {
"type": "Literal",
"value": "finget",
"raw": "'finget'"
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 作用域
作用域是一套规则,用来管理引擎如何查找变量。JS 采用是词法作用域 (lexical scoping),也就是静态作用域,函数的作用域在函数定义的时候就决定了。
与之对应的还有一个动态作用域,函数的作用域是在函数调用的时候才决定的
# 执行阶段
# 执行上下文
JavaScript 中的运行环境有三种。
- 全局环境:JavaScript 代码运行起来会首先进入该环境
- 函数环境:当函数被调用执行时,会进入当前函数中执行代码
- eval
JS 引擎会以函数调用栈 (call stack) 的方式处理代码,遇到以上三种情况,都会分别生成不同类型的执行上下文,并推入调用栈中。所以 JavaScript 中有三种执行上下文类型:
- 全局执行上下文 (只有一个)
- 函数执行上下文
- eval(不常用,不做讨论)
执行上下文可以分为两个阶段:创建阶段和执行阶段。
# 上下文创建阶段
创建阶段做了两件事:
- LexicalEnvironment(词法环境) 组件被创建。
- VariableEnvironment(变量环境) 组件被创建。
// 伪代码
ExecutionContext = {
LexicalEnvironment = { ... }, // 词法环境
VariableEnvironment = { ... }, // 变量环境
}
2
3
4
5
# 词法环境(Lexical Environment)
词法环境是 ECMA 中的一个规范类型 —— 基于代码词法嵌套结构用来记录标识符和具体变量或函数的关联。 简单来说,词法环境就是建立了标识符 - 变量的映射表。这里的标识符指的是变量名称或函数名,而变量则是实际变量原始值或者对象/函数的引用地址。
在词法环境中,有三个组成部分:
- 环境记录 (environment record):存储变量和函数声明的实际位置。
- 对外部环境的引用 (outer environment reference):意味着它可以访问其外部词法环境,用以实现作用域链。
- this 绑定 (This Binding):确定当前环境中 this 的指向;
由于执行上下文有两种类型(全局和函数),所以词法环境也有两种:
- 全局词法环境:
- 包含定义了的全局变量;
- 对外部环境的引用指向 null;
- 拥有全局对象 window,且 this 指向 window;
- 函数词法环境:
- 包含在函数中定义的变量;
- 对外部环境的引用指向生成这个词法的父环境;
- this 的指向调用时确定;
- 有一个 arguments 对象,包含了传给函数的参数。
# 变量环境 Variable Environment
变量环境是一种特定类型的词法环境,词法环境的属性和特点他都有!
在 ES6 前,声明变量都是通过 var 关键词,在 ES6 中则提倡使用 let 和 const,为了兼容 var 的写法,于是使用变量环境来存储 var 声明的变量。
变量环境本质上仍是词法环境,但它只存储 var 声明的变量,这样在初始化变量时可以赋值为 undefined。let、const 生命的变量则会被保持为 uninitialized(未初始化状态)。
# 例子
let a = 10;
const b = 20;
var sum;
function add(e, f){
var d = 40;
return d + e + f
}
let utils = {
add
}
sum = utils.add(a, b)
2
3
4
5
6
7
8
9
10
11
12
13
14
// 执行上下文 - 创建阶段 (伪代码)
// 全局上下文
GlobalExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
type: 'object',
add: <function>,
a: <uninitialized>,
b: <uninitialized>,
utils: <uninitialized>,
},
outer: null,
this: <globalObject>
},
VariableEnvironment: {
EnvironmentRecord: {
type: 'object',
sum: undefined
},
outer: null,
this: <globalObject>
},
}
// 当运行到函数 add 时创建函数执行上下文
FunctionExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
type: 'declarative',
arguments: { 0: 10, 1: 20, length: 2 },
[[ThisValue]]: <utils>,
[[NewTarget]]: undefined,
...
},
outer: <GlobalLexicalEnvironment>, // outer 指向的是当前词法环境被定义时的父环境
this: <utils>
},
VariableEnvironment: {
EnvironmentRecord: {
type: 'declarative',
d: undefined
},
outer: <GlobalLexicalEnvironment>,
this: <utils>
},
}
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
# 与 ES3 的区别
如果你了解 ES5 版本的有关执行上下文的内容,会感到奇怪为啥有关 VO、AO、作用域、作用域链等内容没有在本文中提及。其实两者概念并不冲突,一个是 ES3 规范中的定义,而词法环境则是 ES6 规范的定义。不同时期,不同称呼。
ES3 | ES6 |
---|---|
作用域 | 词法环境 |
作用域链 | outer 引用 |
VO/AO | 环境记录 |
参考:stackoverflow (opens new window)
# 上下文执行阶段
在此阶段,完成了对所有这些变量的分配,并最终执行了代码。
- 赋值
- 词法环境用于解析绑定
- 词法环境:最初,它只是环境变量的一个副本,在运行的上下文中,它用于确定出现在上下文中的标识符的绑定
- 在执行阶段之后,借助词法环境,变量环境表被赋值(填充)
- 每个执行上下文都有用于标识符解析的词法环境。上下文的所有本地绑定都存储于环境记录表中。如果在当前环境记录中没有解析标识符,解析过程将继续道外部(父)环境记录表。此模式将继续,直到标识符被解析位置。如果没有找到,抛出 ReferenceError。
- 这与原型查找链非常相似。现在这里要记住的关键是词法环境在上下文创建(静态地)从词汇上捕获外部绑定,并在运行上下文(执行阶段)中使用。即词法环境是根据代码的位置来决定的,词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。
- 在创建阶段的所有函数都静态地(从词汇上)捕获其父环境的外部绑定。这允许嵌套函数访问外部绑定,即使父上下文已从执行堆栈中清除。这种机制是 JavaScript 中闭包的基础。
还是刚才的例子:
// 执行上下文 - 执行阶段 (伪代码)
// 全局上下文
GlobalExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
type: 'object',
add: <function>,
+ a: 20,
+ b: 30,
+ utils: < func >,
},
outer: null,
this: <globalObject>
},
VariableEnvironment: {
EnvironmentRecord: {
type: 'object',
sum: undefined
},
outer: null,
this: <globalObject>
},
}
// 当运行到函数 add 时创建函数执行上下文
FunctionExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
type: 'declarative',
arguments: { 0: 10, 1: 20, length: 2 },
[[ThisValue]]: <utils>,
[[NewTarget]]: undefined,
...
},
outer: <GlobalLexicalEnvironment>, // outer 指向的是词法环境被定义时的上下文
this: <utils>
},
VariableEnvironment: {
EnvironmentRecord: {
type: 'declarative',
+ d: 20
},
outer: <GlobalLexicalEnvironment>,
this: <utils>
},
}
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
函数完成后,返回值存储在 sum 中。因此,全局词法环境已更新。之后,全局代码完成,程序结束。