说明
- 本文是针对ES6标准来说的(但好像是从ES5就已经这样了),和ES3的标准有区别
- 本来并非原创文章,知识点基本来自参考文章和ES6标准文档的翻译(google)
- 文章中可能存在错误
1 执行上下文是什么
执行上下文(Execution Context,也有翻译是执行环境)是JS中最为重要的一个概念,它定义了变量或者函数有权访问的其他数据,决定了他们各自的行为。 执行环境有三种类型
- 全局执行上下文
是最外围的一个执行上下文,根据执行ESMAScript实现所在的宿主环境不同,表示执行上下文的对象也不一样。在web浏览器中,全局执行上下文被认为是window对象。
某个执行上下文中的所有代码执行完毕后,该环境被销毁,保留在其中的所有变量和函数定义也随之销毁。全局执行上下文直到应用程序退出,例如关闭网页或浏览器时才会被销毁。 - 函数执行上下文
每个函数都有自己的执行上下文 - Eval执行上下文
eval代码特定的环境,本文后续都不涉及。
2 如何创建执行上下文
创建执行环境有两个阶段
2.1 创建阶段
创建阶段会发生三件事:this绑定、创建词法环境组件、创建环境变量组件
2.1.1 this绑定
在全局执行上下文中,this指向全局对象。
在函数执行上下文中,this的值取决于函数调用。如果函数被一个引用对象调用,则this指向那个对象。否则this的值被设置为全局对象或者undefined(严格模式)。
2.1.2 词法环境
描述
词法环境(Lexical Environments)是一种规范类型,用于根据ECMAcript代码的词法嵌套结构定义标识符与特定变量和函数的关联。
一个词法环境由一个环境记录(EnvironmentRecord)和一个对外部词法环境(可能是空)的引用组成。
通常,词法环境与ECMAScript代码的某些特定语法结构相关联,例如FunctionDeclaration、BlockStatement或TryStatement的Catch子句,并且每次评估此类代码都会创建一个新的词法环境
说明
- 这里的标识符指的是变量或函数的名字
- 外部环境的引用指的是它可以访问的父级词法环境
- ECMAScript的类型有两种(8 类型 )
-
语言类型
指的是可以在JS中使用的,比如Null,Number,String、object等 -
规范类型
描述ECMAScript运算的中途结果,这些只不能操作,只存在于规范中,包括Reference、List、Propoerty Identifier、Lexical Environment、Enviroment Record等
-
类型
- 全局环境
没有外部环境引用,环境记录器是对象环境记录器 - 函数环境
环境记录器是声明式环境记录器
环境记录器
Environment Records,是一种规范类型,用于定义标识符与特定变量和函数的关联。
通常,环境记录与代码的某些特定语法结构想关联,例如FunctionDeclaration、BlockStatement 或 TryStatement 的 Catch 子句。和上面词法环境的描述差不多,因为就是它的主要组成部分
环境记录有三种类型
- 声明式环境记录器(Declarative Environment Record)
绑定范围内的标识符级,包含let、const、class、module、function等。包含两个子类:- 函数环境记录器(Function Environment Records )
- 模块环境记录器(Module Environment Records)
- 对象环境记录器(Object Environment Record)
定义出现在全局执行环境中的变量和函数的关系 - 全局环境记录器(Global Environment Record)
用于脚本全局声明,[[outerEnv]]为空(这个不知道干啥用的)。可以预先填充标识符绑定,并包括一个关联的全局对象。
2.1.3 变量环境
它同样是一个词法环境,其环境记录器记录变量声明语句在执行上下文中创建的绑定关系。
在ES6中,词法环境组件和变量环境组件的区别是,前者用来存储函数声明和let、const变量绑定,后者用来存储var变量绑定。
2.1.4 Demo
const assign = require('lodash/assign');
const user = {
nick: '李向维',
sex: '女',
};
let a = 20;
const b = 10;
var c;
function add(x, y) {
var result = x + y;
return result;
}
c = add(a, b);
console.log(assign(user));
全局执行上下文
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnviromentRecord: {
Type: "Object",
// 绑定标识符
a: <uninitialized>,
b: <uninitialized>,
user: <uninitialized>,
lodash: { // Module Environment Records
Type: "Module",
assign: { // Function Environment Records
Type: "Function",
Arguments: null,//vs debug的时候是这样显示的
length: 0,//vs debug的时候是这样显示的
}
...
},
add: { // Function Environment Records
Type: "Function",
Arguments: {},// 自己写的 可能有误
length: 2
},
},
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
c: undefined,
},
outer: <null>
}
}
当调用到add函数时,会创建函数执行上下文
FunctionExecutionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
Arguments: {0: 20, 1: 10, length: 2},
x: 20,
y: 10,
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Delcarative",
result: undefined,
},
outer: <GlobalLexicalEnvironment>
}
}
注意到这里,var声明的和let、const声明的变量的设置值不一样。反映到代码运行中,就是var定义的变量可以在声明之前访问,也就是变量提升,但let和const声明的不行,会报错。
2.2 执行阶段
在此阶段,完成对变量的分配,然后执行代码。
注意,此时如果JS引擎不能在源码中声明的实际位置找到let变量的值,它会被赋值为undefined。
3 如何管理执行上下文
全局上下文只有一个,但在代码执行中可能会有很多的函数上下文,这些上下文又是如何被管理的?
执行上下文栈(Execation Context Stack),是用来管理执行上下文的数据结构,存储了代码执行期间所有的执行上下文。
它是有大小的,入栈的上下文太多,可能会发生栈溢出。这在递归中容易出现。
用开发工具的debug可以观察到栈的进入和移出,this指向等。