# 前言
突然觉得对于一名JavaScript开发者而言,需要知道JavaScript程序内部是如何运行的,那么对于此章节执行上下文和执行栈的理解很重要,对理解其他JavaScript概念(变量声明提示,作用域和闭包)都有帮助。
看了很多相关文章,写得很好,总结了ES3以及ES6对于执行上下文概念的描述,以及新的概念介绍。
# 什么是执行上下文
简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。
# 执行上下文的类型
JavaScript 中有三种执行上下文类型
- 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置
this的值等于这个全局对象。一个程序中只会有一个全局执行上下文。 - 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
- Eval 函数执行上下文 — 执行在
eval函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用eval,所以在这里我不会讨论它。
# ES3 执行上下文的内容
执行上下文是一个抽象的概念,我们可以将它理解为一个 object ,一个执行上下文里包括以下内容:
- 变量对象 VO
- 活动对象 AO
- 作用域链
- 调用者信息 this
# 变量对象(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)
作用域 规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做 作用域链。
# 当前可执行代码块的调用者(this)
如果当前函数被作为对象方法调用或使用 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
}
# ES3中的执行上下文生命周期
执行上下文的生命周期有三个阶段,分别是:
- 创建阶段
- 执行阶段
- 销毁阶段
# 创建阶段
函数执行上下文的创建阶段,发生在函数调用时且在执行函数体内的具体代码之前,在创建阶段,JS 引擎会做如下操作:
# 全局执行上下文
-
执行全局代码前,创建一个全局执行上下文
-
对全局数据进行预处理
- 这一阶段会进行变量和函数的初始化声明
- var 定义的全局变量–> undefined 添加为window属性
- function 声明的全局函数 –-> 赋值(fun) 添加为window属性
- this --> 赋值(window)
# 函数执行上下文
- 在调用函数时,准备执行函数体之前,创建对应的函数执行上下文对象
- 对局部数据进行预处理
- 形参变量==》赋值(实参)–》添加为执行上下文的属性
- arguments–>赋值–>(实参列表),添加为执行上下文属性
- var 定义的局部变量 –-> undefined 添加为执行上下文属性
- function 神明的函数 --> 赋值(fun) 添加为执行上下文属性
- 构建作用域链(前面已经说过构建细节)
- this --> 赋值(调用函数对象)
有没有发现这个创建执行上下文的阶段有变量和函数的初始化生命。这个操作就是 **变量声明提升**(变量和函数声明都会提升,但是函数提升更靠前)。
# 执行阶段
执行阶段中,JS 代码开始逐条执行,在这个阶段,JS 引擎开始对定义的变量赋值、开始顺着作用域链访问变量、如果内部有函数调用就创建一个新的执行上下文压入执行栈并把控制权交出……
# 销毁阶段
一般来讲当函数执行完成后,当前执行上下文(局部环境)会被弹出执行上下文栈并且销毁,控制权被重新交给执行栈上一层的执行上下文。
注意这只是一般情况,闭包的情况又有所不同。
闭包的定义:有权访问另一个函数内部变量的函数。简单说来,如果一个函数被作为另一个函数的返回值,并在外部被引用,那么这个函数就被称为闭包。
# ES3执行上下文总结
对于 ES3 中的执行上下文,我们可以用下面这个列表来概括程序执行的整个过程:
- 函数被调用
- 在执行具体的函数代码之前,创建了执行上下文
- 进入执行上下文的创建阶段:
- 初始化作用域链
- 创建
arguments object检查上下文中的参数,初始化名称和值并创建引用副本 - 扫描上下文找到所有函数声明:
- 对于每个找到的函数,用它们的原生函数名,在变量对象中创建一个属性,该属性里存放的是一个指向实际内存地址的指针
- 如果函数名称已经存在了,属性的引用指针将会被覆盖
- 扫描上下文找到所有var的变量声明:
- 对于每个找到的变量声明,用它们的原生变量名,在变量对象中创建一个属性,并且使用
undefined来初始化 - 如果变量名作为属性在变量对象中已存在,则不做任何处理并接着扫描
- 对于每个找到的变量声明,用它们的原生变量名,在变量对象中创建一个属性,并且使用
- 确定
this值
- 进入执行上下文的执行阶段:
- 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。
# ES5中的执行上下文
ES5 规范又对 ES3 中执行上下文的部分概念做了调整,最主要的调整,就是去除了 ES3 中变量对象和活动对象,以 词法环境组件( LexicalEnvironment component) 和 变量环境组件( VariableEnvironment component) 替代。所以 ES5 的执行上下文概念上表示大概如下:
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
# This Binding
- 全局执行上下文中,
this的值指向全局对象,在浏览器中this的值指向window对象,而在nodejs中指向这个文件的module对象。 - 函数执行上下文中,
this的值取决于函数的调用方式。具体有:默认绑定、隐式绑定、显式绑定(硬绑定)、new绑定、箭头函数,具体内容会在【this全面解析】部分详解。
# 词法环境(Lexical Environment)
词法环境有两个组成部分
- 1、环境记录:存储变量和函数声明的实际位置
- 2、对外部环境的引用:可以访问其外部词法环境
词法环境有两种类型
- 1、全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,
this的值指向这个全局对象。 - 2、函数环境:用户在函数中定义的变量被存储在环境记录中,包含了
arguments对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。
直接看伪代码可能更加直观
