在写本篇文章之前,本人阅读了一些文章和书籍(文末),每篇文章都写的合理且准确,但是在脑子中联系起来就很抽象,所以在此先根据自己的理解做一些流程讲解,有不足或不准确之处敬请指出
总体概述
此部分只为了先从大体上了解执行上下文大致的框架是怎样的,具体的名词细节下文讲解。
执行上下文是一个抽象的概念,当 js 引擎解析到可执行代码片段时,会做一些准备工作,这个准备工作就叫执行上下文- 执行上下文类型分为三种:
全局执行上下文—— 文件第一次加载的默认执行上下文函数执行上下文—— 函数调用生成的上下文,每次调用都会创建- eval 函数执行上下文 —— eval 函数内的执行上下文,不常用,所以暂不涉及
执行上下文栈是用来管理执行上下文的一个栈结构,遵循LIFO(后进先出)特性,程序的执行流就是通过这个上下文栈进行控制的。- 文件第一次加载,则全局执行上下文
入栈 - 遇到函数调用,则函数执行上下文
入栈 - 函数调用完毕,函数执行上下文
出栈 - 文件执行完毕或关闭浏览器,全局执行上下文
出栈
- 文件第一次加载,则全局执行上下文
- 在这里涉及到执行上下文的生命周期,分为三个阶段
- 创建阶段
a. 全局执行上下文:执行全局代码之前,创建一个全局的执行上下文
b. 函数执行上下文:在调用函数,但并未执行函数体之前,创建对应的函数执行上下文 - 执行阶段 —— 逐条执行 JS 代码
- 销毁阶段
a. 全局执行上下文:程序退出前(关闭网页或退出浏览器)
b. 函数上下文:当前函数执行完毕,该函数上下文弹出栈
- 创建阶段
- 顺理成章,就想要知道执行上下文里到底是什么内容,在 ES3 和 ES5 中,对执行上下文内容的描述略有不同,但是这些概念大同小异。
- ES3 中,执行上下文包括:
变量对象(活动对象),作用域链,this - ES5 中,执行上下文包括:
this,词法环境,变量环境
- ES3 中,执行上下文包括:
- 接下来,就要对执行上下文的内容做一个详细的讲解了,下文的前半部分是对上述的一些简述概念的详解,后面还有完整的示例解析。
1 执行上下文
1.1 什么是执行上下文
执行上下文(Execution context 简称 EC)就是 js 的执行环境,它包括 this 的值、变量、对象和函数。它是一个抽象的概念。
变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。
例如,当执行到可执行代码片段(通常是函数调用)的时候,JS 引擎会做一些“准备工作”,而这个“准备工作”,就叫做 执行上下文。
1.2 执行上下文的类型
Javascript 中有三种执行上下文类型:
- 全局执行上下文
- Global execution context (
GEC) - 是默认的执行上下文,文件第一次加载到浏览器中,js 代码在默认执行上下文中开始执行
- 一个程序中只会存在一个全局上下文
- 全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器
- 全局上下文是 最外层的上下文,根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样:1. 浏览器中,全局上下文是 window 对象,因此通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法;2. node 环境中,全局上下文是 global
- Global execution context (
- 函数执行上下文
- Functional execution context (
FEC) - 函数调用时,会创建一个函数执行上下文
- 函数被重复调用,每次调用都会创建一个新的执行上下文
- Functional execution context (
- Eval 函数执行上下文 ———— eval 函数内的执行上下文
2 执行上下文栈
执行上下文栈(Execution context stack 简称 ECS)是执行 js 代码时创建的执行栈结构,遵循 LIFO(后进先出)特性,用于管理在代码执行期间创建的执行上下文。
- GEC(全局执行上下文)在栈最底层
- 当 JS 引擎发现一处函数调用,会创建一个 FEC(函数执行上下文),并
push进栈,将控制权交给该上下文 - 在函数执行完之后,上下文栈弹出(
pop)该 FEC(函数执行上下文),并将控制权返还给之前的执行上下文
例如:下面这段代码
var a = 10;
function funcA() {
console.log("Start function A");
function funcB(){
console.log("In function B");
}
funcB();
}
funcA();
console.log("GlobalContext");
- 代码执行之前,JS 引擎将
全局执行上下文推入执行上下文栈 - funcA 在全局上下文中被调用,
funcA 的执行上下文入栈,并运行该函数 - 在 funcA 的上下文中,调用了 funcB 函数,
funcB 的执行上下文入栈,并运行该函数 - funcB 函数中所有代码执行完毕,
funcB 函数的上下文出栈,继续执行 funcA 函数 - funcA 函数中所有代码执行完毕,
funcA 函数的上下文出栈,继续执行全局上下文中的代码 - 所有代码执行完毕,则全局上下文弹出
入栈和出栈示意图如下:
3 执行上下文的生命周期
执行上下文的生命周期有三个阶段,分别是
- 创建阶段
- 执行阶段
- 销毁阶段
3.1 创建阶段
3.1.1 创建时机
-
全局上下文的创建时机为 执行全局代码之前。
-
函数执行上下文的创建时机为,在 调用函数时,但还未开始执行函数体内的具体代码之前。
3.1.2 创建内容
-
全局上下文:对全局数据进行预处理,包括:
变量初始化声明、函数初始化声明、this赋值。这些行为都包含在全局上下文的变量对象、作用域链和this的值中。 -
函数上下文:对局部数据进行预处理,不仅包括全局上下文中提到的
变量初始化声明、函数初始化声明、this赋值,还有函数上下文独有的参数列表 arguments。这些行为都包含在函数上下文的变量对象、作用域链和this的值中。
3.2 执行阶段
- 在这个阶段,JS 代码开始逐条执行
- JS 引擎开始对定义的变量
赋值(初始化时,由于变量提升,各变量的值均为 undefined),开始顺着作用域链访问变量 - 如果内部有函数调用,就创建一个新的执行上下文压入执行栈,并把控制权交出
3.3 销毁阶段
-
全局上下文:在应用程序退出前会被销毁,比如关闭网页或退出浏览器。
-
函数上下文:一般来讲,当函数执行完毕之后,当前函数执行上下文就会被弹出执行上下文栈,并被销毁,控制权被重新交给之前的上下文。(但这只是一般情况,当有闭包的时候,销毁时机有所不同,此文中不探讨)
4 执行上下文的内容
执行上下文是一个抽象的概念,可以用一个对象的数据结构来模拟。
而 ES3 和 ES5 中的执行上下文中包括的内容略有不同,ES3 中的变量对象与活动对象的概念,在 ES5 之后由词法环境和变量环境来解释,两者概念不冲突,后者理解更为通俗易懂。
4.1 ES3 中执行上下文的内容
ES3 中的每一个执行上下文都有三个重要的属性:
- 变量对象 VO
- 作用域链
- this 的指向
4.1.1 创建变量对象
每个执行上下文都有一个表示变量的对象 ———— 变量对象(variable object 简称 VO),全局上下文的变量对象始终存在,而函数上下文只会在函数执行的过程中存在
- 全局上下文:其中的变量对象就是全局对象,在
浏览器中是 window 对象,在node 环境中,是 global 对象 - 函数上下文:其中的变量对象我们用
活动对象 AO(activation object)来表示,二者的区别就是:活动对象就是变量对象,只不过处于不同的状态和阶段而已。函数没有被调用的时候,我们不能访问变量对象中的属性和方法,而当函数被调用,变量对象被激活为活动对象,这些属性和方法可以被访问。
变量对象包含的内容有
- 根据当前函数的
参数列表(arguments)初始化一个 arguments 对象(全局上下文没有 arguments ) - 根据
函数声明生成对应的属性,其值为一个指向内存中函数的引用指针,如果函数名已存在,则覆盖 - 根据
变量声明生成对应的属性,由于变量声明提升,此时初始值为 undefined,需要等到执行阶段才会有确定的值。如果变量名已声明,则忽略该变量声明
示例
var c = 1;
function funA (a, b) {
var c = 3;
return a - b - c;
}
funA(7, 1);
当 调用 funcA 和 执行 funcA 前 的这段时间(也就是创建阶段),JS 引擎为 funcA 创建了一个变量对象如下:
VO = {
argumentObject : {
0: a,
1: b,
length: 2
},
a: 7,
b: 1
c: undefined // 由于变量提升,此阶段 c 的值为 undefined
}
- argumentObject 代表入参,funcA 有两个入参,因此 length 属性值为 2。0 和 1 分别对应着两个入参 a 和 b
- a 和 b 两个入参的值已经确定,因此 a 初始化为 7,b 初始化为 1
- 函数中的变量 c 由于变量提升会被初始化为 undefined,可以用下面代码来解释:
var c; // 变量声明提升
c = 1;
function funA (a, b) {
var c; // 变量声明提升
// 函数执行(函数上下文的执行阶段)才会给变量赋值
c = 3;
return a - b - c;
}
funA(7, 1);
4.1.2 创建作用域链
- 由多个执行上下文的 变量对象 构成的链表叫做作用域链。
- js 通过作用域及作用域链来进行变量查询。
- 当查找变量时,首先从 当前上下文的变量对象 中查找,如果没有,就会从 父级(词法层面的父级)的执行上下文的变量对象 中查找,一直找到全局上下文的变量对象,也就是全局对象。
这里需要对几个概念进行讲解:
- 词法作用域
- 作用域链是如何构成的
4.1.2.1 词法作用域
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
其核心概念就是:函数的作用域在函数定义的时候就确定了,与函数在哪里调用或被谁调用都没有关系。
如下示例:
var a = 1;
function funA() {
console.log(a);
}
function funB() {
var a = 2;
funA();
}
funB(); // 此时应该打印 1 还是 2 呢?
- 由于 JS 采用词法作用域,所以 funA() 函数的作用域在定义的时候就已经确定了,是 全局作用域。
- 在执行 funA() 时,会查找打印变量 a
- 虽然是在 funB() 中调用的 funA(),但是前面说过,与被谁调用无关。所以会先在 funA() 本身内部 查找是否存在变量 a,答:不存在。那么会去 funA() 的词法层面的父级作用域 查找,也就是 全局作用域 ,全局作用域中是否存在变量 a,答:存在,为 1,所以打印结果为 1
4.1.2.2 作用域链是如何构成的
作用域链是由作用域构成的链式结构。
用与上面同样的示例来讲解一下作用域链是如何构建的:
1 var a = 1;
2
3 function funA() {
4 console.log(a);
5 }
6
7 function funB() {
8 var a = 2;
9 funA();
10 }
11
12 funB(); // 此时应该打印 1 还是 2 呢?
在 funA() 函数声明(3 行)的时候,内部会绑定一个 [[scope]] 的内部属性:
funA.[[scope]] = [
globalContext.VO
]
在调用了 funA() 但并未执行 funA() 中代码之前(9 行),会创建 funA() 的执行上下文,并入上下文栈。创建上下文时会创建作用域链,流程如下:
- 复制函数的
[[scope]]属性初始化作用域链 - 创建变量对象
- 将变量对象压入作用域链的最顶端
1. 初始化作用域链
// 初始化作用域链
funAContext = {
scope: funA.[[scope]]
}
2. 创建变量对象
// 创建变量对象
funAContext = {
scope: funA.[[scope]],
VO = {
arguments: {
length: 0
}
}
}
3. 将变量对象压入作用域链的最顶端
// 将变量对象压入作用域链的最顶端
funAContext = {
scope: [VO, funA.[[scope]]],
VO = {
arguments: {
length: 0
}
}
}
4.1.2.3 示例
var c = 1;
function funA (a, b) {
var c = 3;
return a - b - c;
}
funA(7, 1);
结果输出 3
4.1.3 确定 this 的值
- 全局执行上下文中,this 的值指向全局对象
- 函数执行上下文中,this 的值取决于函数的调用方式,谁调用它 this 就指向谁(除非用
bindcallapply等API进行委托调用,this 会指向通过 API 传入的对象)
4.2 用一个示例来演示 ES3 中执行上下文的创建流程
4.3 ES5 中执行上下文的内容
ES5 规范去除了 ES3 中变量对象和活动对象,以 词法环境组件( LexicalEnvironment component) 和 变量环境组件( VariableEnvironment component) 替代。
上下文的内容包含三部分:
- this 的指向
- 词法环境(LexicalEnvironment) 组件
- 变量环境(VariableEnvironment) 组件
伪代码大概如下:
ExecutionContext = {
ThisBinding = <this value>, // 确定this
LexicalEnvironment = { ... }, // 词法环境
VariableEnvironment = { ... }, // 变量环境
}
4.3.1 this 的指向
与 ES3 中的 this 的指向(绑定)没什么区别。
4.3.2 创建词法环境组件
词法环境有 两种类型:
- 全局环境:是一个没有外部环境的词法环境,外部环境引用为
null - 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了
arguments对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。
词法环境有 两个组成部分:
- 环境记录:存储变量和函数声明的实际位置
- 对外部环境的引用:它指向作用域链的下一个对象,可以访问其父级词法环境(作用域),作用与 es3 的作用域链相似
伪代码如下:
// 词法环境类型一:全局环境
GlobalExectionContext = { // 全局执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 词法环境组成部分一:环境记录
Type: "Object", // 全局环境
// 标识符绑定在这里(变量等)
}
outer: <null> // 词法环境组成部分二:对外部环境的引用
}
}
// 词法环境类型二:函数环境
FunctionExectionContext = { // 函数执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 词法环境组成部分一:环境记录
Type: "Declarative", // 函数环境
// 标识符绑定在这里(变量等)
}
outer: <Global or outer function environment reference> // 词法环境组成部分二:对外部环境的引用
}
}
4.3.3 创建变量环境组件
变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。
在 ES6 中,词法环境与变量环境的区别在于:
- 词法环境:存储
函数声明和使用letconst关键字绑定的变量,以此来实现函数级作用域 - 变量环境:存储
var声明的变量