https://juejin.im/post/5ba321...
https://juejin.im/entry/59986...
我只是搬运工,看了他们的文章后深有启发,于是把他们的精华汇总然后加入自己的理解整理了这一篇文章。
这是一个非常抽象的概念,你无需彻底的弄明白它的意思,你只需要明白它做了什么。
在充分理解他做了什么之前还是要了解一下它到底是什么
Execution Context(执行上下文)是 ECMA-262 标准中定义的一个抽象概念,用于同 Executable
Code(可执行代码)进行区分。
1:什么是执行代码----Executable Code
合法的,可以被解释器解析执行的代码。
分为三类
- Global Code:全局代码
- Function Code:函数体内的代码
- Eval Code:使用 eval() 函数执行的代码
2:什么是执行上下文----Execution Context
执行上下文 是 ES 用来跟踪代码运行状态和相关资源集合的特殊机制。它决定了执行代码执行的过程中可以访问的数据。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。
分为三类
- Global Execution Context:全局执行上下文
这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中执行。它会执行两件事:创建一个全局的 window
对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
- Function Execution Context:函数执行上下文
每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数被调用时都有它自己的执行上下文。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
- Eval Execution Context:eval() 函数执行上下文
由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。
3:执行上下文的基本工作方式
先理解两个名词:执行上下文栈(Execution Context Stack)、运行执行上下文(Running Execution Context)
执行上下文栈( Execution Context Stack ):用来保存所有执行上下文的栈,是一种拥有 LIFO(后进先出)数据结构的栈。 当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
运行执行上下文( Running Execution Context ):正在使用的执行上下文。在任意时间,最多只能有一
个正在运行代码的执行上下文。
4:基本工作方式
运行执行上下文总是在执行上下文栈的顶部,全局执行上下文总在执行上下文栈的底部。无论什么时候,只要控制权从与当前运行执行上下文相关的可执行代码上切换到另一部分与当前运行执行上下文不相关的可执行代码上,一个新的执行上下文就会被创建,新创建的执行上下文会被放在当前的运行执行上下文的上面,成为新的运行执行上下文。
5:具体工作流程
如前言中提到的,ES 标准中并没有从技术实现的角度定义执行上下文准确类型和结构,为了更方便地解释
执行代码和执行上下文之间的关系,暂且用数组表示执行上下文栈,然后用伪代码来操作执行上下文栈:
DCStack = [] // 执行上下文栈
<1:开始执行代码:全局执行代码与全局执行上下文
解析器在解析执行代码时首先执行全局代码,为其创建对应的执行上下文,全局上下文被压入执行上下文栈
ECStack = [
globalContext // 全局执行上下文
]
<2:开始执行函数:函数代码与函数执行上下文
注意:函数代码中不包括内部函数的代码
运行下面的函数
(function foo(bar) {
if (bar) {
return
}
foo(true);
})()
我们用伪代码还原一下执行栈中发生了什么??
// 第一次调用 foo
ECStack = [
<foo> functionContext,
globalContext
]
// 第二次调用 foo
ECStack = [
<foo> functionContext – recursively(递归),
<foo> functionContext,
globalContext
]
我们看一个实际的例子
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
首先执行这段代码,解析器解析到了这段代码,于是先创建了一个全局上下文,并把全局上下文压入执行栈
ECStack = [
Global Context
]
然后解析器检测到了 first(),开始调用first函数,于是创建了一个first函数上下文,并把这个函数向下文压入到执行栈的顶部(一般执行栈的顶部都是正在运行的上下文,现在正在调用first函数,所以顶部就是他的上下文)
ECSstack= [
First Function Context-----(顶部是正在执行的上下文)
Global Context
]
在first() 函数内部又调用了second()函数,于是JavaScript 引擎为second()函数创建了一个属于他的执行上下文,并把它压入执行栈的最顶部。(因为现在执行second()函数,所以他的执行上下文就在最顶部,因为first()函数没有执行完所以他的执行上下文依然在执行栈的队列中)
ECSstack = [
Cecond Function Context-----(顶部是正在执行的上下文)
First Function Context
Global Context
]
执行完second()函数之后,它的执行上下文会自动从执行栈弹出,并且控制流程执行下一个执行上下文,即 first() 函数的执行上下文。
ECSstack= [
First Function Context-----(顶部是正在执行的上下文)
Global Context
]
当 first() 执行完毕,它的执行上下文自动从栈弹出,控制流程按顺序到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。
ECStack = [
Global Context
]
6:JavaScript 引擎是怎么创建执行上下文?
创建执行上下文有两个阶段:1>:创建阶段 和 2>:执行阶段。
1>:创建阶段--(The Creation Phase)
在创建阶段会发生三件事
ExecutionContext = {
ThisBinding = <this value>, // this绑定
LexicalEnvironment = { ... }, // 词法环境
VariableEnvironment = { ... }, // 变量环境
}
- This 绑定。
在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this引用 Window 对象)。在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined(在严格模式下)。例如:
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 引用 'foo', 因为 'baz' 被对象 'foo' 调用
let bar = foo.baz;
bar(); // 'this' 指向全局 window 对象,因为没有指定引用对象
- 创建词法环境组件。
词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。有点没明白
简单来说词法环境是一种定义标识符以及变量的嵌套结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。
在词法环境的内部有两个部件组成:
1:环境记录器:是存储变量和函数声明的实际位置。:2: 外部环境的引用:意味着它可以访问其父级词法环境(作用域)。
词法环境有两种类型:
1:全局环境:(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的
Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且
this的值指向全局对象。2:函数环境:函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。
环境记录器也有两种类型:
1:声明式环境记录器存储变量、函数和参数。2:对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。
简而言之,
环境记录器在全局环境中,环境记录器是对象环境记录器。 在函数环境中,环境记录器是声明式环境记录器。
注意
函数环境,声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射和传递给函数的参数的length)
抽象地讲,词法环境在伪代码中看起来像这样:
GlobalExectionContext = { // 全局执行上下文
LexicalEnvironment: { // 词法环境组件
EnvironmentRecord: { // 环境记录器 ---对象环境记录器
Type: "Object",
// 在这里绑定标识符
}
outer: <null> // 外部环境引用, 是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。
}
}
FunctionExectionContext = { // 函数执行上下文
LexicalEnvironment: { // 词法环境组件
EnvironmentRecord: { // 环境记录器 ---声明式环境记录器
Type: "Declarative",
// 在这里绑定标识符
}
outer: <Global or outer function environment reference> //外部环境引用 函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。
}
}
- 创建变量环境组件。
变量环境也是一个词法环境。所以它有着上面定义的词法环境的所有属性,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
在 ES6 中,词法环境组件和变量环境组件的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。
来个栗子
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
执行上下文用伪函数这么表示
// 全局执行上下文
GlobalExectionContext = {
1:ThisBinding: <Global Object>, //this绑定
2: LexicalEnvironment: { // 词法环境 --全局的词法环境
EnvironmentRecord: { //环境记录器
Type: "Object",
// 在这里绑定标识符
a: < uninitialized >, // 变量a的绑定(let)
b: < uninitialized >, // 变量b 的绑定(const)
multiply: < func > // 函数声明
}
outer: <null> // 外部环境的引用nul
},
3: VariableEnvironment: { // 变量环境 --全局的词法环境
EnvironmentRecord: { //环境记录器
Type: "Object",
// 在这里绑定标识符
c: undefined, // 变量c 的绑定(var)
}
outer: <null> // 外部环境的引用nul
}
}
// 函数的执行上下文-----(只有遇到调用函数 multiply 时,函数执行上下文才会被创建)
FunctionExectionContext = {
1:ThisBinding: <Global Object>, // this 绑定
2:LexicalEnvironment: { //词法环境 --函数的词法环境
EnvironmentRecord: { // 环境记录器
Type: "Declarative",
// 在这里绑定标识符
Arguments: {0: 20, 1: 30, length: 2}, // 声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储函数参数键值对和传递给函数的参数的length)。
},
outer: <GlobalLexicalEnvironment> // 外部环境的引用是全局环境
},
3:VariableEnvironment: { //变量环境
EnvironmentRecord: { // 环境记录器
Type: "Declarative",
// 在这里绑定标识符
g: undefined // 变量g的绑定(var)
},
outer: <GlobalLexicalEnvironment> // 外部环境的引用是全局环境
}
}
可能你已经注意到 let 和 const 定义的变量并没有关联任何值,但 var 定义的变量被设成了 undefined。
这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefined(var
情况下),或者未初始化(let 和 const 情况下)。 这就是为什么你可以在声明之前访问 var 定义的变量(虽然是
undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。 这就是我们说的变量声明提升。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。