1. 前言
JavaScript 本身是一种“解释型”语言。它不像Java会先将代码编译成二进制文件,再交给虚拟机去执行;JavaScript引擎则是在要执行一段代码时,会先去“评估”这段代码,之后才会在执行线程中去运行这段代码。而在评估代码时,会将代码运行时所需变量、函数、this等数据准备好。这个准备数据的过程就是在创建一个执行环境,而该环境通常是被包含在执行上下文(Execution Contexts--EC) 中。
在介绍执行上下文前,首先我们要知道:
- JavaScript解析器在运行
js代码时是单线程的执行机制,因此在同一时间点上,只能有一段代码(函数)在执行; - 当运行一段JavaScript代码之前,JS解析器会创建一个执行环境(执行上下文)。它是运行代码前做一些准备工作,为的是代码执行时已经备好所需要的信息(变量、函数、this等)。
2. 可执行代码Executable Code
当一个JS应用程序启动后,就会执行一段“可执行代码”;当用户触发交互时,应用程序同时触发具体某一事件,此时也会执行一段“可执行代码”。因此要理解执行上下文,首先你要知道“可执行代码Executable Code”。
一共有三种 ECMAScript 可执行代码:
- 全局代码是指写在一个 JavaScript 文件中或者嵌入 HTML 中的可被 ECMA 脚本程序处理的源代码文本,这些源代码文本不包括函数体部分的源代码文本。
<script>
var str = 'guojing';
function fn(){
// 这里就不是全局代码,因为在函数体里
console.log(str);
}
fn()
</script>
- Eval 代码是指传递给 eval 函数的参数部分的源代码文本,此时参数部分将成为可执行的代码。
eval('alert("hello, world.")'); // 参数就是Eval代码
- 函数代码是指作为函数体FunctionBody被解析的源代码文本,这里不包括嵌套的函数体代码。
function test(){
// 这里。函数体内部的代码就是函数代码
var a = 10;
console.log(a);
}
3. 环境记录
环境记录是一种规范类型,用于在 ECMAScript 代码中基于词法嵌套结构上定义特定变量和函数标识符的关联关系。
通常,词法环境会与 ECMAScript 代码诸如 FunctionDeclaration、WithStatement 或者 TryStatement 的 Catch 块这样的特定句法结构相联系,且类似代码每次执行都会有一个新的环境记录被创建出来。
每个环境记录都有一个 [[OuterEnv]] 字段,它是 空值或对外部环境记录的引用。这用于对环境记录值的逻辑嵌套进行建模。(内部)环境记录的外部引用是对逻辑上围绕内部环境记录的环境记录的引用。当然,外部环境记录可以有自己的外部环境记录。一个环境记录可以作为多个内部环境记录的外部环境。例如,如果一个函数声明 包含两个嵌套 函数声明 然后每个嵌套函数的环境记录将具有作为其外部环境记录的当前评估周围函数的环境记录。
3.1 环境记录类型
环境记录 可以被认为是一个简单的面向对象的层次结构中,其中 环境记录 是具有三个具体子类的抽象类: 声明式环境记录, 对象环境记录 和 全局环境记录。 其中函数环境记录 和 模块环境记录 是 声明式环境记录 的子类。
环境记录分为:
声明式环境记录 Declarative Environment Records
- 函数环境记录 Function Environment Records
- 模块环境记录 Module Environment Records
对象环境记录 Object Environment Records
全局环境记录 Global Environment Records
3.2 声明式环境记录
每个声明性环境记录都与一个 ECMAScript 程序作用域相关联,其中包含变量、常量、let、类class、模块module、import和函数声明。声明性环境记录绑定由其范围内包含的声明定义的标识符集。
简单来说,声明式环境记录用于变量声明(var、const、let)、类、模块、导入或函数声明等定义的标识符与其值之间的绑定关系。
其中函数环境记录对应ECMAScript调用的函数对象,并包含函数内部声明的所有绑定。同时可能绑定this,以及支持super调用时所需的状态。
而模块环境记录包含其顶部所有的声明绑定,以及显式导入的其他模块的绑定。其外部环境引用[[OuterEnv]]是一个全局环境记录。
3.3 对象环境记录
每个对象环境记录都与一个称为其绑定对象的对象相关联。对象环境记录绑定一组字符串标识符名称,这些名称直接对应于其绑定对象的属性名称。不是字符串的属性键不包含在绑定标识符集中。无论 [[Enumerable]] 属性的设置如何,自己的和继承的属性都包含在集合中。
对象环境记录一般用于在WithStatement中直接将一系列标识符与其绑定对象的属性名称建立一一对应关系。
| 字段名称 | 值 | 意义 |
|---|---|---|
| [[BindingObject]] | Object | 绑定对象 |
| [[IsWithEnvironment]] | Boolean | 表示这是否 环境记录是为with语句创建的。 |
var withObj = {name: 'haha'}
with(withObj){
console.log(name); // 'haha'
name = 'heihei';
console.log(name);
console.log(constructor);
}
// 对象环境记录描述
// ObjectRecord => {[[BindingObject]]: withObj, [[IsWithEnvironment]]: true}
3.4 函数环境记录
函数环境记录是声明性环境记录,用于表示函数的顶级范围,如果函数不是 ArrowFunction,则提供 this 绑定。如果函数不是 ArrowFunction 函数并引用 super,则其函数环境记录还包含用于从函数内部执行 super 方法调用的状态。
| 字段名称 | 值 | 含义 |
|---|---|---|
| [[ThisValue]] | Any | 函数调用时this的值 |
| [[ThisBindingStatus]] | lexical or initialized or uninitialized | 如果值为lexical,则这是一个箭头函数,没有本地 this 值 |
| [[FunctionObject]] | Object | 创建当前函数环境记录的函数对象 |
| [[NewTarget]] | Object or undefined | 如果此环境记录是由 [[Construct]] 内部方法创建的,[[NewTarget]] 是 [[Construct]] newTarget 参数的值。否则,它的值是未定义的。 |
3.5 全局环境记录
全局环境记录用于表示所有 ECMAScript 脚本元素共享的最外层作用域范围。全局环境记录为内置全局变量、全局对象的属性以及脚本中出现的所有顶级声明提供绑定。
全局环境记录在逻辑上是单一记录,但它被指定为封装对象环境记录和声明性环境记录的组合。对象环境记录将全局对象作为其基础对象。全局环境记录的对象环境记录组件包含所有内置全局变量的绑定以及全局代码中包含的 FunctionDeclaration、GeneratorDeclaration、AsyncFunctionDeclaration、AsyncGeneratorDeclaration 或 VariableStatement 引入的所有绑定。全局代码中所有其他 ECMAScript 声明的绑定包含在全局环境记录的声明性环境记录组件中。
总结来说,全局环境记录用于全局代码的声明绑定,它在任何 ECMA 脚本的代码执行前创建。一个全局环境记录被指定为一个复合封装 对象环境记录 和一个 声明式环境记录 的一个单一的记录。没有外部环境引用,即它的 [[OuterEnv]] 是空值null。
| 字段名称 | 值 | 意义 |
|---|---|---|
| [[ObjectRecord]] | 对象环境记录 | 绑定对象是一个全局对象。它包含全局内置绑定以及关联领域的全局代码中FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration绑定 |
| [[GlobalThisValue]] | Object | 在全局作用域内返回的this值。宿主可以提供任何ECMAScript对象值。 |
| [[DeclarativeRecord]] | 声明性环境记录 | 包含在关联领域的全局代码中除了FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration绑定之外的所有声明的绑定 |
| [[VarNames]] | 字符串的列表 | 关联领域的全局代码中的FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration声明绑定的字符串名称。 |
这里强调一下FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration不在Declarative Environment Record中,而是在Object Environment Record中,这也解释了为什么在全局代码中用var、function声明的变量自动的变为全局对象的属性而let、const、class等声明的变量却不会成为全局对象的属性。
var x = 1; // VariableDeclaration
function f(){} // FunctionDeclaration
console.log(y) // 暂存死区 Uncaught ReferenceError: Cannot access 'y' before initialization
let y = 10;
const z = 'z';
// 代码执行之前创建环境记录
GER = {
ObjectRecord: {BindingObject: {...window, x:undefined, f:'<func>' }, IsWithEnvironment: false, },
GlobalThisValue:window,
DeclarativeRecord: {
y:<uninitialized> // 当代码指定到 声明语句"let y" 才会初始化为undefined
z:<uninitialized>
},
VarNames: ['x', 'f']
}
同时也帮助我们理解什么是变量和函数声明提升以及let/const暂存死区。
var x = 1; // VariableDeclaration
function f() {} // FunctionDeclaration
// console.log(y);
let y;
console.log(y); // undefined
y = 10;
const z = 'z';
2.6 模块环境记录
模块环境记录是一个声明性环境记录,用于表示ECMAScript模块的外部作用域。除了正常的可变和不可变绑定之外,模块环境记录还提供了不可变的导入绑定,这些绑定提供间接访问另一个环境记录中存在的目标绑定。
2.7 对象结构描述环境记录
// 全局环境记录
GlobalEnvironmentRecord = {
ObjectRecord: {BindingObject: window, IsWithEnvironment: false, },
GlobalThisValue:window,
DeclarativeRecord: {
},
VarNames: [],
OuterEnv: null
}
// 函数环境记录
FunctionEnvironmentRecord = {
ThisValue: <any>,
ThisBindingStatus: lexical | uninitialized | initialized,
FunctionObject: <func>
}
2.8 环境记录的常用操作
ECMAScript规范中,常见的操作如下:
2.8.1 GetIdentifierReference ( env, name, strict )
功能:获取标识符的引用。
参数:env 一个环境记录或者null;name 字符串,标识符名称;strict bool是否为严格模式。
它在调用时执行以下步骤:
-
如果
env为 null值, 则返回 对象{ [[Base]]: unresolvable, [[ReferencedName]]:name, [[Strict]]:strict, [[ThisValue]]: empty } -
定义变量
exists,值为?env.HasBinding(name). -
如果
exists为 true, 则返回 对象 { [[Base]]:env, [[ReferencedName]]:name, [[Strict]]:strict, [[ThisValue]]: empty }. -
否则
a. 定义变量
outer,值为env.[[OuterEnv]].b. 返回 ? GetIdentifierReference(
outer,name,strict).
2.8.2 NewDeclarativeEnvironment ( E )
功能:实例化一个声明式环境记录
参数:E 一个环境记录
它在调用时执行以下步骤:
-
定义变量
env值为 一个新的不包含任何绑定的声明式环境记录 -
设置
env.[[OuterEnv]] 为E. -
返回
env.
2.8.3 NewObjectEnvironment ( O, E )
功能: 实例化一个对象环境记录
参数:O 一个对象;E 一个环境记录
它在调用时执行以下步骤:
-
定义变量
env值为 一个新的对象环境记录,并且其绑定对象为O -
设置
env.[[OuterEnv]] 为E. -
返回
env.
2.8.4 NewFunctionEnvironment ( F, newTarget )
功能:实例化一个函数环境记录
参数:F 函数对象; newTarget 一个对象
它在调用时执行以下步骤:
-
断言:
F是一个 ECMAScript 函数. -
断言: Type(
newTarget) 返回值为 Undefined 或 Object. -
定义变量
env值为一个新的没有任何绑定的函数环境记录. -
设置
env.[[FunctionObject]] 为F. -
如果
F.[[ThisMode]] 是 lexical, 设置env.[[ThisBindingStatus]] 为 lexical. -
否则, 设置
env.[[ThisBindingStatus]] 为 uninitialized. -
设置
env.[[NewTarget]] 为newTarget. -
设置
env.[[OuterEnv]] 为F.[[Environment]]. -
返回
env.
2.8.5 NewGlobalEnvironment ( G, thisValue )
功能:实例化一个全局环境记录
参数:G 一个对象;thisValue this值
它在调用时执行以下步骤:
-
定义变量
objRec值为 一个新的对象环境记录,其绑定对象为G. -
定义变量
dclRec值为 一个新的无任何绑定的声明式环境记录. -
定义变量
env值为 一个新的全局环境记录. -
设置
env.[[ObjectRecord]] 为objRec. -
设置
env.[[GlobalThisValue]] 为thisValue. -
设置
env.[[DeclarativeRecord]] 为dclRec. -
设置
env.[[VarNames]] 为 一个空的 List. -
设置
env.[[OuterEnv]] to null. -
返回
env.
3. 执行上下文EC
执行上下文是一种规范设备,用于跟踪ECMAScript代码的运行时评估。
在任何时候,每个代理至多有一个实际执行代码的执行上下文。这称为代理的运行执行上下文。
在实际中,可能会创建多个执行上下文,此时需要一个叫做执行上下文栈,用于跟踪执行上下文。正在运行的执行上下文始终是此栈的顶部元素。每当JS引擎从与当前运行的执行上下文相关联的可执行代码转移到与该执行上下文无关的可执行代码时,就会创建新的执行上下文。新创建的执行上下文被压入栈并成为运行执行上下文。
var a = 10;
function fn(){
var b = 'b';
console.log(b);
}
fn();
console.log(a);
首先,把全局的执行上下文记为gec,再把fn函数的执行上下文记为fec,执行上下文栈记为ecStack,正在运行的执行上下文rec,
- 起初ecStack是空的,
- 当执行全局代码时gec被创建,并unshift到ecStack中,ecStack=[gec],rec=ecStack[0]。
- 当进入fn函数调用时,fec被创建并unshift到ecStack中,ecStack=[fec,gec],ec=ecStack[0]。
- Fn函数执行完毕,ecStack.shift(),fec从ecStack中删除,ecStack=[gec],rec=ecStack[0]。
- 执行完毕,ecStack.shift(),gec从ecStack中删除,ecStack又变为空了,rec=ecStack[0]。
执行上下文包含跟踪其关联代码的执行进度所需的任何实现特定状态。所有执行上下文都有下表的组件:
| 组件 | 含义 |
|---|---|
| 代码评估状态 | 执行、暂停和恢复与此执行上下文关联的代码的评估所需的任何状态。 |
| Function | 如果这个执行上下文正在评估一个函数对象的代码,那么这个组件的值就是那个函数对象。如果上下文正在评估脚本或模块的代码,则该值为空。 |
| Realm | 关联代码访问ECMAScript资源的领域记录。 |
| ScriptOrModule | 关联代码的来源是模块记录(Module Record)或脚本记录(Script Record)。如果都不是,则值为null。 |
在运行的执行上下文的Realm组件的值也被称为当前的Realm Record。这里简单给大家说一下领域,领域的主要作用就是为我们开发者提供内置的全部对象。比如Object、Date、Array等等。但不包括宿主环境提供的那些对象(比如console、location)
正在运行的执行上下文的Function组件的值也被称为活动函数对象。
ECMAScript代码的执行上下文具有下表列出的其他状态组件。
| 组件 | 含义 |
|---|---|
| LexicalEnvironment | 标识在此执行上下文中用于解析代码中所有的标识符引用的词法环境。 |
| VariableEnvironment | 标识在此执行上下文中的变量环境,它的环境记录保存了由VariableStatements创建的绑定。 |
执行上下文的 LexicalEnvironment 和 VariableEnvironment 组件始终是环境记录。
4. 如何运用
与执行上下文相关的概念这里就介绍完了,但是相信很多人还是不知道这些概念如何在实际中组合运用起来的。那么这里简单总结了一下大致的流程。
当JS引擎,
-
执行 全局代码 时会创建并进入一个新的执行环境。
-
每次调用函数也会创建并进入一个新的执行环境,即便函数是自身递归调用的。
-
每次调用 eval函数 同样会创建并进入一个新的执行环境
-
每一次 return 都会退出一个执行环境。抛出异常也可退出一个或多个执行环境。
当进入一个执行环境时,会设置该执行环境的 this 绑定、定义变量环境和初始词法环境,并执行声明式绑定初始化过程。
像这样,当程序
-
进入
全局代码的执行环境时,- 初始化执行上下文。
- 使用全局代码执行声明式绑定初始化化步骤。
-
进入函数代码
根据一个函数对象 F、调用者提供的 thisArg 以及调用者提供的 argumentList,进入[函数代码]的执行环境时,
- 如果[函数代码]是严格模式下的代码,设 this 绑定为 thisArg。
- 否则如果 thisArg 是 null 或 undefined,则设 this 绑定为全局对象。
- 否则如果 Type(thisArg) 的结果不为 Object,则设 this 绑定 为 ToObject(thisArg)。
- 否则设 this 绑定为 thisArg。
- 以 F 的 [[Environment]] 内部属性为参数调用 NewDeclarativeEnvironment,并令 localEnv 为调用的结果。
- 设 词法环境组件为 localEnv。
- 令 code 为 F 的 [[Code]]内部属性的值。
- 使用函数代码 code 和 argumentList 执行声明式绑定初始化步骤。
-
声明式绑定初始化
每个执行上下文都有一个关联的 [变量环境组件]。当在一个执行环境下评估一段 ECMA 脚本时,var变量定义会以绑定的形式添加到这个 [变量环境组件]的[环境记录]中。对于函数代码,除了var定义变量其他的包括参数会以绑定的形式添加到这个 [词法环境组件] 的[环境记录]中。
5. 实际应用
>接下来,我们一起通过执行山下文来解决一些作用域相关的面试题。
```js
var x = 10;
function fn(){
console.log(x);
}
function show(f){
var x = 20;
f();
}
show(fn);
```
首先,上述代码没有在任何函数体中,我们就认为是”全局代码“。在评估和执行前,先要创建一个”执行上下文“并设为运行执行上下文REC,并把它叫做全局执行上下文。
// 这里只添加一些相关组件
GEC = {
LexicalEnvironment: {
[[ObjectRecord]]: {
[[BindingObject]]: window
},
[[GlobalThisValue]]: window,
[[DeclarativeRecord]]: {},
[[VarNames]]: [],
[[OuterEnv]]: null
},
VariableEnvironment: {}
}
通过评估代码,我们知道代码中仅有var和function声明,而这些声明都会通过环境记录中的对象环境记录中绑定关系。因此相当于给window对象添加了x属性,默认初始化绑定值为undefined;接着创建fn函数对象,同时设置fn.[[Eviroment]]为当前运行执行上下文的词法环境组件,给window添加fn属性,值为该函数对象;最后创建show函数对象,同时设置show.[[Enviroment]]为当前运行执行上下文的词法环境组件,给window添加show属性,值为该函数对象。
GEC = {
LexicalEnvironment: {
[[ObjectRecord]]: {
[[BindingObject]]: window{x:undefind, fn: '<func>', show: '<func>'}
},
[[GlobalThisValue]]: window,
[[DeclarativeRecord]]: {},
[[VarNames]]: [],
[[OuterEnv]]: null
},
VariableEnvironment: {}
}
当代码执行x = 10时,会将全局执行上下文的x绑定值设为10;
GEC = {
LexicalEnvironment: {
[[ObjectRecord]]: {
[[BindingObject]]: window{x:10, fn: '<func>', show: '<func>'}
},
[[GlobalThisValue]]: window,
[[DeclarativeRecord]]: {},
[[VarNames]]: [],
[[OuterEnv]]: null
},
VariableEnvironment: {}
}
当执行到show(fn)代码时,由于要开始执行函数代码,因此先创建一个新的执行上下文(showFEC),并设为运行执行上下文。
// 函数show执行上下文
showFEC = {
LexicalEnvironment: {
[[ThisValue]]: any,
[[ThisBindingStatus]]: uninitialized,
[[FunctionObject]]: show,
[[OuterEnv]]: GEC.lexical
},
VariableEnvironment: {}
}
通过评估show函数体内部代码,发现仅有一个var声明语句以及一个参数f,因此只需要在词法环境中添加绑定即可。
showFEC = {
LexicalEnvironment: {
[[ThisValue]]: window, // 非严格模式
[[ThisBindingStatus]]: initialized,
[[FunctionObject]]: show,
x: undefined,
f: fn,
[[OuterEnv]]: GEC.lexical
},
VariableEnvironment: {}
}
当show函数开始执行代码体时,首先是x = 20,此时将记录中的x绑定值修改为20.
showFEC = {
LexicalEnvironment: {
[[ThisValue]]: window, // 非严格模式
[[ThisBindingStatus]]: initialized,
[[FunctionObject]]: show,
x: 20,
[[OuterEnv]]: GEC.lexical
},
VariableEnvironment: {}
}
紧接着开始调用f,通过调用GetIdentifierReference以及GetBindingValue即可从词法环境中获取f绑定值fn。然后在执行fn函数前,依旧要创建一个新的执行上下文,并设为运行执行上下文。
与show函数类似:
fnFEC = {
LexicalEnvironment: {
[[ThisValue]]: window, // 非严格模式
[[ThisBindingStatus]]: initialized,
[[FunctionObject]]: fn,
[[OuterEnv]]: GEC.lexical
},
VariableEnvironment: {}
}
此时会发现,showFEC和fnFEC的外部环境记录都是全局执行上下文的词法环境,因为两个函数定义时,都处于全局执行环境下。
ok。开始执行f函数体代码。console.log(x),此时要获取变量 x 的值,首先在当前词法环境中查找,没有从其外部环境记录找继续查找,直到全局执行上下文的词法环境,如果没有找到就会引发引用异常,找到了就直接返回值。
因此,在fnFEC.lexical中没有,就会去GEC.lexical中查找,此时得到x的值为10。因此最终输出结果就是10.
6. 总结
执行上下文是JavaScript中最重要最关键的知识点。如果这里搞通了,那么this值情况,作用域相关、闭包相关的知识点也就同样通畅了。
如果这里大家感觉还是不太理解,可以多多通过面试题去反复锤炼。把这篇文章中设计到的点都能运用灵活,那么你就真正搞懂了JavaScript的运行机制了。
这里还有一些面试题,大家可以继续玩一玩啦。
6.1 来个简单的
var a = 10;
function f(a){
console.log(a)
var a = 20;
console.log(a)
}
f(a)
6.2 进阶变式
function f(a){
console.log(a)
var a = 20;
function a(){}
console.log(a);
}
f(100)
6.3 超变态
var x = 1;
function f(x,y = function () {x = 3;console.log(x);}) {
console.log(x);
var x = 2;
y();
console.log(x);
}
f();
console.log(x);
看看结果,一定令你意向不到...