突然觉得对于一名JavaScript开发者而言,需要知道JavaScript程序内部是如何运行的,那么对于此章节执行上下文和执行栈的理解很重要,对理解其他JavaScript概念(变量声明提示,作用域和闭包)都有帮助。
看了很多相关文章,写得很好,总结了ES3以及ES6对于执行上下文概念的描述,以及新的概念介绍。
简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。
JavaScript 中有三种执行上下文类型
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
)函数进入执行阶段时,原本不能访问的变量对象被激活成为一个活动对象,自此,我们可以访问到其中的各种属性。
「其实变量对象和活动对象是一个东西,只不过处于不同的状态和阶段而已。」
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
中的执行上下文,我们可以用下面这个列表来概括程序执行的整个过程:
undefined
来初始化arguments object
检查上下文中的参数,初始化名称和值并创建引用副本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
中的执行上下文,我们可以用下面这个列表来概括程序执行的整个过程:
var
定义的变量,初始值为 undefined
造成声明提升)let
和 const
定义的变量)this
值为全局对象(以浏览器为例,就是 window
)var
定义的变量,初始值为 undefined
造成声明提升)let
和 const
定义的变量)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');
js执行栈
上述代码的执行上下文栈。
当上述代码在浏览器加载时,JavaScript 引擎创建了一个全局执行上下文并把它压入当前执行栈。当遇到 first()
函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。
当从 first()
函数内部调用 second()
函数时,JavaScript 引擎为 second()
函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当 second()
函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即 first()
函数的执行上下文。
当 first()
执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。