文章主要参考了文末列出的一些相关文章,加上了一部分自己的理解和对其他文章的整理,有不足之处敬请指出
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 functionA() {
console.log("Start function A");
function functionB(){
console.log("In function B");
}
functionB();
}
functionA();
console.log("GlobalContext");
复制代码
- 代码执行之前,JS 引擎将
全局执行上下文
推入执行上下文栈 - funcA 在全局上下文中被调用,
funcA 的执行上下文入栈
,并运行该函数 - 在 funcA 的上下文中,调用了 funcB 函数,
funcB 的执行上下文入栈
,并运行该函数 - funcB 函数中所有代码执行完毕,
funcB 函数的上下文出栈
,继续执行 funcA 函数 - funcA 函数中所有代码执行完毕,
funcA 函数的上下文出栈
,继续执行全局上下文中的代码 - 所有代码执行完毕,则全局上下文弹出
入栈和出栈示意图如下:
3 创建执行上下文
以上介绍了 JS 引擎如何处理
执行上下文(push 和 pop),下面介绍 JS 引擎如何创建
执行上下文。
这个过程分为两个阶段:
- 创建阶段
- 执行阶段
3.1 创建阶段
函数执行上下文的创建阶段,发生在函数调用时,但还未开始执行函数体内的具体代码之前。
在这个阶段,JS 对整个函数进行了一个编译,主要做了以下三件事:
- 创建
变量对象
variable object
简称VO
- 根据当前函数的
参数列表(arguments)
初始化一个 arguments 对象 - 根据
函数声明
生成对应的属性,其值为一个指向内存中函数的引用指针,如果函数名已存在,则覆盖 - 根据
变量声明
生成对应的属性,由于变量声明提升,此时初始值为 undefined,需要等到执行阶段才会有确定的值。如果变量名已声明,则忽略该变量声明 - 虽然无法通过代码访问变量对象,但是后台处理数据会用到它
- 创建
作用域链
- 变量对象创建完,JS 引擎就开始初始化作用域链
- 确定
this
的值- 全局执行上下文中,this 的值指向全局对象
- 函数执行上下文中,this 的值取决于函数的调用方式
注意:只有 函数声明 会被加入到变量对象中,函数表达式 会被忽略
// 函数声明,会被加入变量对象
function a () {}
// b 是变量声明,也会被加入变量对象,但是作为一个函数表达式 _b 不会被加入变量对象
var b = function _b () {}
复制代码
3.1.1 示例
下面通过一个例子来说明
function funA (a, b) {
var c = 3;
var d = 2;
d = function() {
return a - b;
}
}
funA(3, 2);
复制代码
当调用 funcA 和执行 funcA 前的这段时间(也就是创建阶段),JS 引擎为 funcA 创建了一个变量对象如下:
executionContextObj = {
variableObject: {}, // funA 中所有的变量,参数和内部函数
scopeChain: [], // 当前函数涉及的作用域的列表
this
}
复制代码
变量对象包含的内容如下:
variableObject = {
argumentObject : {
0: a,
1: b,
length: 2
},
a: 3,
b: 2
c: undefined,
d: undefined // 执行到第 6 行时,改为 pointer to the function defintion of d
}
复制代码
- argumentObject 代表入参,funcA 有两个入参,因此 length 属性值为 2。0 和 1 分别对应着两个入参 a 和 b
- a 和 b 两个入参的值已经确定,因此 a 初始化为 3,b 初始化为 2
- 函数中的变量 c 和 d 会被初始化为 undefined
- 执行到 d = function() {…} 时,d 的值会被修改为指向堆内存中的一段函数定义
3.2 执行阶段
- 在这个阶段,原本不能访问的变量对象被激活成一个
活动对象
(activation object
简称AO
),自此,我们可以访问到其中的各种属性 - JS 引擎开始对定义的变量
赋值
,开始顺着作用域链访问变量 - 如果内部有函数调用,就创建一个新的执行上下文压入执行栈,并把控制权交出
变量对象和活动对象是一个东西,只不过处于不同的状态和阶段而已
上一段示例中,在执行阶段完成后,变量对象的内容改为:
variableObject = {
argumentObject : {
0: a,
1: b,
length: 2
},
a: 3,
b: 2,
c: 3,
d: undefined then pointer to the function defintion of d
}
复制代码
4 完整示例
完整示例链接查看:
Execution context, Scope chain and JavaScript internals 中 Complete example 一节。
代码执行过程中的变量对象变化如下:
a = 1;
var b = 2;
cFunc = function(e) {
var c = 10;
var d = 15;
a = 3
function dFunc() {
var f = 5;
}
dFunc();
}
cFunc(10);
复制代码
全局编译阶段:
globalExecutionContextObj = {
activationbj: {
argumentObj : {
length:0
},
b: undefined,
cFunc: Pointer to the function definition
},
scopeChain: [GLobal execution context variable object],
this: value of this
}
复制代码
全局执行阶段:
globalExecutionContextObj = {
activationbj: {
argumentObj : {
length:0
},
b: 2,
cFunc: Pointer to the function definition,
a: 1
},
scopeChain: [GLobal execution context variable object],
this: value of this
}
复制代码
cFunc的编译阶段:
cFuncExecutionContextObj = {
activationbj: {
argumentObj : {
0: e,
length:1
},
e: 10,
c: undefined,
d: undefined
dFunc: Pointer to the function definition,
},
scopeChain: [cFunc variable object, Global exection context variable object],
this: value of this
}
复制代码
cFunc的执行阶段:
cFuncExecutionContextObj = {
activationbj: {
argumentObj : {
0: e,
length:1
},
e: 10,
c: 10,
d: 15
dFunc: Pointer to the function definition,
},
scopeChain: [cFunc variable object, Global exection context variable object],
this: value of this
}
复制代码
5 作用域链
作用域链
是当前函数所在的 “变量对象” 的链表。
- 当查找变量的时候,会先从当前上下文的变量对象中查找
- 如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找
- 一直找到全局上下文的变量对象