忽然以为对于一名JavaScript开发者而言,须要知道JavaScript程序内部是如何运行的,那么对于此章节执行上下文和执行栈的理解很重要,对理解其余JavaScript概念(变量声明提示,做用域和闭包)都有帮助。javascript
看了不少相关文章,写得很好,总结了ES3以及ES6对于执行上下文概念的描述,以及新的概念介绍。java
简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。node
JavaScript 中有三种执行上下文类型express
this
的值等于这个全局对象。一个程序中只会有一个全局执行上下文。eval
函数内部的代码也会有它属于本身的执行上下文,但因为 JavaScript 开发者并不常用 eval
,因此在这里我不会讨论它。执行上下文是一个抽象的概念,咱们能够将它理解为一个 object
,一个执行上下文里包括如下内容:编程
variable object
简称 VO
)每一个执行环境文都有一个表示变量的对象——变量对象,全局执行环境的变量对象始终存在,而函数这样局部环境的变量,只会在函数执行的过程当中存在,在函数被调用时且在具体的函数代码运行以前,JS 引擎会用当前函数的参数列表(arguments
)初始化一个 “变量对象” 并将当前执行上下文与之关联 ,函数代码块中声明的 变量 和 函数 将做为属性添加到这个变量对象上。数组
有一点须要注意,只有函数声明(function declaration)会被加入到变量对象中,而函数表达式(function expression)会被忽略。
复制代码
// 这种叫作函数声明,会被加入变量对象
function demo () {}
// tmp 是变量声明,也会被加入变量对象,可是做为一个函数表达式 demo2 不会被加入变量对象
var tmp = function demo2 () {}
复制代码
全局执行上下文和函数执行上下文中的变量对象还略有不一样,它们之间的差异简单来讲:浏览器
window
对象。VO
)被激活为活动对象(AO
)时,咱们才能访问到其中的属性和方法。activation object
简称 AO
)函数进入执行阶段时,本来不能访问的变量对象被激活成为一个活动对象,自此,咱们能够访问到其中的各类属性。bash
其实变量对象和活动对象是一个东西,只不过处于不一样的状态和阶段而已。数据结构
scope chain
)做用域 规定了如何查找变量,也就是肯定当前执行代码对变量的访问权限。当查找变量的时候,会先从当前上下文的变量对象中查找,若是没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫作 做用域链。闭包
若是当前函数被做为对象方法调用或使用 bind
call
apply
等 API
进行委托调用,则将当前代码块的调用者信息(this value
)存入当前执行上下文,不然默认为全局对象调用。
关于 this
的建立细节,有点烦,有兴趣的话能够进入 这个章节 学习。
若是将上述一个完整的执行上下文使用代码形式表现出来的话,应该相似于下面这种:
executionContext:{
[variable object | activation object]:{
arguments,
variables: [...],
funcions: [...]
},
scope chain: variable object + all parents scopes
thisValue: context object
}
复制代码
执行上下文的生命周期有三个阶段,分别是:
函数执行上下文的建立阶段,发生在函数调用时且在执行函数体内的具体代码以前,在建立阶段,JS 引擎会作以下操做:
执行全局代码前,建立一个全局执行上下文
对全局数据进行预处理
有没有发现这个建立执行上下文的阶段有变量和函数的初始化生命。这个操做就是 **变量声明提高**(变量和函数声明都会提高,可是函数提高更靠前)。
复制代码
执行阶段中,JS 代码开始逐条执行,在这个阶段,JS 引擎开始对定义的变量赋值、开始顺着做用域链访问变量、若是内部有函数调用就建立一个新的执行上下文压入执行栈并把控制权交出……
通常来说当函数执行完成后,当前执行上下文(局部环境)会被弹出执行上下文栈而且销毁,控制权被从新交给执行栈上一层的执行上下文。
注意这只是通常状况,闭包的状况又有所不一样。
闭包的定义:有权访问另外一个函数内部变量的函数。简单说来,若是一个函数被做为另外一个函数的返回值,并在外部被引用,那么这个函数就被称为闭包。
对于 ES3
中的执行上下文,咱们能够用下面这个列表来归纳程序执行的整个过程:
arguments object
检查上下文中的参数,初始化名称和值并建立引用副本undefined
来初始化this
值ES5
规范又对 ES3
中执行上下文的部分概念作了调整,最主要的调整,就是去除了 ES3
中变量对象和活动对象,以 词法环境组件( LexicalEnvironment component) 和 变量环境组件( VariableEnvironment component) 替代。因此 ES5
的执行上下文概念上表示大概以下:
ExecutionContext = {
ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, } 复制代码
this
的值指向全局对象,在浏览器中this
的值指向 window
对象,而在nodejs
中指向这个文件的module
对象。this
的值取决于函数的调用方式。具体有:默认绑定、隐式绑定、显式绑定(硬绑定)、new
绑定、箭头函数,具体内容会在【this全面解析】部分详解。词法环境有两个组成部分
词法环境有两种类型
this
的值指向这个全局对象。arguments
对象。对外部环境的引用能够是全局环境,也能够是包含内部函数的外部函数环境。直接看伪代码可能更加直观
GlobalExectionContext = { // 全局执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Object", // 全局环境
// 标识符绑定在这里
outer: <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 = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
复制代码
变量提高的缘由:在建立阶段,函数声明存储在环境中,而变量会被设置为 undefined
(在 var
的状况下)或保持未初始化(在 let
和 const
的状况下)。因此这就是为何能够在声明以前访问 var
定义的变量(尽管是 undefined
),但若是在声明以前访问 let
和 const
定义的变量就会提示引用错误的缘由。这就是所谓的变量提高。
对于 ES5
中的执行上下文,咱们能够用下面这个列表来归纳程序执行的整个过程:
let
和 const
定义的变量)var
定义的变量,初始值为 undefined
形成声明提高)this
值为全局对象(以浏览器为例,就是 window
)let
和 const
定义的变量)var
定义的变量,初始值为 undefined
形成声明提高)this
值执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时建立的全部执行上下文。
当 JavaScript 引擎第一次遇到你的脚本时,它会建立一个全局的执行上下文而且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数建立一个新的执行上下文并压入栈的顶部。
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
让咱们经过下面的代码示例来理解:
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');
复制代码
上述代码的执行上下文栈。
当上述代码在浏览器加载时,JavaScript 引擎建立了一个全局执行上下文并把它压入当前执行栈。当遇到 first()
函数调用时,JavaScript 引擎为该函数建立一个新的执行上下文并把它压入当前执行栈的顶部。
当从 first()
函数内部调用 second()
函数时,JavaScript 引擎为 second()
函数建立了一个新的执行上下文并把它压入当前执行栈的顶部。当 second()
函数执行完毕,它的执行上下文会从当前栈弹出,而且控制流程到达下一个执行上下文,即 first()
函数的执行上下文。
当 first()
执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦全部代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。