https://juejin.im/post/5ba321...
https://juejin.im/entry/59986...
我只是搬运工,看了他们的文章后深有启发,因而把他们的精华汇总而后加入本身的理解整理了这一篇文章。数组
这是一个很是抽象的概念,你无需完全的弄明白它的意思,你只须要明白它作了什么。浏览器
在充分理解他作了什么以前仍是要了解一下它究竟是什么数据结构
Execution Context(执行上下文)是 ECMA-262 标准中定义的一个 抽象概念,用于同 Executable
Code(可执行代码)进行区分。
1:什么是执行代码----Executable Codeide
合法的,能够被解释器解析执行的代码。
分为三类函数
2:什么是执行上下文----Execution Contextpost
执行上下文 是 ES 用来 跟踪代码运行状态和相关资源集合的特殊机制。它 决定了执行代码执行的过程当中能够访问的数据。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。this
分为三类code
这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中执行。它会执行两件事:建立一个全局的 window
对象(浏览器的状况下),而且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
每当一个函数被调用时, 都会为该函数建立一个新的上下文。每一个函数被调用时都有它本身的执行上下文。函数上下文能够有任意多个。每当一个新的执行上下文被建立,它会按定义的顺序(将在后文讨论)执行一系列步骤。
因为 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引用 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 的变量会获得一个引用错误。 这就是咱们说的 变量声明提高。