JS 执行上下文
一、含义:
当 JS
引擎解析到可执行代码片段(通常是函数调用阶段)的时候,就会先做一些执行前的准备工作,这个 "准备工作" ,就叫做 "执行上下文(execution context 简称 EC
)" 或者也可以叫做执行环境。
二、类型(ES3)
-
全局执行上下文 :这是默认或者说是最基础的执行上下文,一个程序中只会存在一个全局上下文,它在整个
javascript
脚本的生命周期内都会存在于执行堆栈的最底部不会被栈弹出销毁。全局上下文会生成一个全局对象(以浏览器环境为例,这个全局对象是window
),并且将this
值绑定到这个全局对象上。 -
函数执行上下文:每当一个函数被调用时,都会创建一个新的函数执行上下文(不管这个函数是不是被重复调用的)
-
Eval 函数执行上下文 :执行在
eval
函数内部的代码也会有它属于自己的执行上下文,但由于并不经常使用eval
,所以在这里不做分析。
三、包括(ES3)
1. 变量对象VO(variable object
)
VO
即Variable Object 变量对象,存储全局变量
和函数
,例如:
css
let a = 1
let arr = [1,2,3]
let obj = {id:107}
function fn(){ ... }
// globalEC
globalEC = {
VO:{
a: 1,
arr: [1,2,3],
obj: {id:107},
fn: function fn(){ ... }
}
}
有一点需要注意,只有函数声明(function declaration)会被加入到变量对象中,而函数表达式(function expression)会被忽略。
php
// 这种叫做函数声明,会被加入变量对象
function a () {}
// b 是变量声明,也会被加入变量对象,但是作为一个函数表达式 _b 不会被加入变量对象
var b = function _b () {}
全局执行上下文和函数执行上下文中的变量对象还略有不同,它们之间的差别简单来说:
(1) 全局上下文中的变量对象就是全局对象 ,以浏览器环境来说,就是 window
对象。
(2) 函数执行上下文中的变量对象内部定义的属性 ,是不能被直接访问的,只有当函数被调用时,变量对象(VO
)被激活为活动对象(AO
)时,我们才能访问到其中的属性和方法。
2. 活动对象AO(activation object
)
AO
即Activation Object 活跃对象,,存储局部变量
和子函数
以及arguments
。
函数进入执行阶段时,原本不能访问的变量对象被激活成为一个活动对象,自此,我们可以访问到其中的各种属性。 其实变量对象和活动对象是一个东西,只不过处于不同的状态和阶段而已。
例如:
scss
function fn(a,b){
var c = 3,
var fn2 = function(){
let d = 4
console.log(a+b+c+d)
}
fn2()
}
fn(1,2) // 10
// fn函数开始执行前,创建fnEC
fnEC = {
AO:{
arguments:{
'0':1,
'1':2,
length:2
},
a:1,
b:2,
c:undefined,
fn2:undefined,
}
}
// 将fnEC推入执行上下文栈
ECStack.push(fnEC) // [globalEC,fnEC]
// fn函数执行的过程中慢慢填装AO
fnEC = {
AO:{
arguments:{ ... },
a:1,
b:2,
c:3,
fn2:function(){ ... }
}
}
// 执行内部函数fn2,也是如此
fn2EC = {
AO:{
arguments:{
length:0
},
d:4
}
}
// 将fn2EC推入栈
ECStack.push(fn2EC) // [globalEC,fnEC,fn2EC]
// 执行fn2结束
ECStack.pop() // fn2EC销毁
// 执行fn结束
ECStack.pop() // fnEC销毁
3. 作用域链Scope
作用域 规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做 作用域链。
函数的作用域在函数创建时就已经确定了。当函数创建时,会有一个名为 [[scope]]
的内部属性保存所有父变量对象到其中。当函数执行时,会创建一个执行环境,然后通过复制函数的 [[scope]]
属性中的对象构建起执行环境的作用域链,然后,变量对象 VO
被激活生成 AO
并添加到作用域链的前端,完整作用域链创建完成:
lua
Scope = [AO].concat([[Scope]]);
4. 调用者this
如果当前函数被作为对象方法调用或使用 bind
call
apply
等 API
进行委托调用,则将当前代码块的调用者信息(this value
)存入当前执行上下文,否则默认为全局对象调用。
总结:
如果将上述一个完整的执行上下文使用代码形式表现出来的话,应该类似于下面这种:
less
executionContext:{
[variable object | activation object]:{
arguments,
variables: [...],
funcions: [...]
},
scope chain: variable object + all parents scopes
thisValue: context object
}
四、生命周期(ES3)
1.创建阶段
2.执行阶段
3.销毁阶段
五、总结(ES3)
对于 ES3
中的执行上下文,我们可以用下面这个列表来概括程序执行的整个过程:
1. 函数被调用
2. 在执行具体的函数代码之前,创建了执行上下文
3. 进入执行上下文的创建阶段:
1.初始化作用域链
2.创建 arguments object
检查上下文中的参数,初始化名称和值并创建引用副本
3.扫描上下文找到所有函数声明:
(1)对于每个找到的函数,用它们的原生函数名,在变量对象中创建一个属性,该属性里存放的是一个指向实际内存地址的指针
(2)如果函数名称已经存在了,属性的引用指针将会被覆盖
4.扫描上下文找到所有 var
的变量声明:
(1)对于每个找到的变量声明,用它们的原生变量名,在变量对象中创建一个属性,并且使用 undefined
来初始化
(2)如果变量名作为属性在变量对象中已存在,则不做任何处理并接着扫描
- 确定
this
值
4.进入执行上下文的执行阶段:
1.在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。
六、ES5 中的执行上下文
ES5
规范又对 ES3
中执行上下文的部分概念做了调整,最主要的调整,就是去除了 ES3
中变量对象和活动对象,以 词法环境组件( LexicalEnvironment component
) 和 变量环境组件( VariableEnvironment component
) 替代。所以 ES5
的执行上下文概念上表示大概如下:
ini
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
变量环境 它也是一个 词法环境 ,所以它有着词法环境的所有特性。
之所以在 ES5
的规范力要单独分出一个变量环境的概念是为 ES6
服务的: 在 ES6
中,词法环境 组件和 变量环境 的一个不同就是前者被用来存储函数声明和变量(let
和 const
)绑定,而后者只用来存储 var
变量绑定。
在上下文创建阶段,引擎检查代码找出变量和函数声明,变量最初会设置为 undefined
(var 情况下),或者未初始化(let 和 const 情况下)。这就是为什么你可以在声明之前访问 var
定义的变量(虽然是 undefined),但是在声明之前访问 let
和 const
的变量会得到一个引用错误。
七、ES5 执行上下文总结
对于 ES5
中的执行上下文,我们可以用下面这个列表来概括程序执行的整个过程:
-
程序启动,全局上下文被创建
-
创建全局上下文的 词法环境
(1) 创建 对象环境记录器 ,它用来定义出现在 全局上下文 中的变量和函数的关系(负责处理
let
和const
定义的变量)(2) 创建 外部环境引用 ,值为
null
-
创建全局上下文的 变量环境
(1)创建 对象环境记录器 ,它持有 变量声明语句 在执行上下文中创建的绑定关系(负责处理
var
定义的变量,初始值为undefined
造成声明提升)(2)创建 外部环境引用 ,值为
null
-
确定
this
值为全局对象(以浏览器为例,就是window
)
-
-
函数被调用,函数上下文被创建
-
创建函数上下文的 词法环境
(1)创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的
arguments
对象(此对象存储索引和参数的映射)和传递给函数的参数的 length 。(负责处理let
和const
定义的变量)(2)创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
-
创建函数上下文的 变量环境
(1)创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的
arguments
对象(此对象存储索引和参数的映射)和传递给函数的参数的 length 。(负责处理var
定义的变量,初始值为undefined
造成声明提升)(2)创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
-
确定
this
值
-
-
进入函数执行上下文的执行阶段:
- 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。
八、闭包
闭包的定义:有权访问另一个函数内部变量的函数。简单说来,如果一个函数被作为另一个函数的返回值,并在外部被引用,那么这个函数就被称为闭包。
九、this
this
在函数执行时
才确认,本质上为函数的实际调用者
- 当没有调用者时,默认调用者为
window
this
存储在AO
中- 箭头函数的
AO
没有this
十、总结:
- 当函数运行的时候,会生成一个叫做 "执行上下文" 的东西,也可以叫做执行环境,它用于保存函数运行时需要的一些信息。
- 所有的执行上下文都会被交给系统的 "执行上下文栈" 来管理,它是一个栈结构数据,全局上下文永远在该栈的最底部,每当一个函数执行生成了新的上下文,该上下文对象就会被压入栈,但是上下文栈有容量限制,如果超出容量就会栈溢出。
- 执行上下文内部存储了包括:变量对象 、作用域链 、this 指向 这些函数运行时的必须数据。
- 变量对象构建的过程中会触发变量和函数的声明提升。
- 函数内部代码执行时,会先访问本地的变量对象去尝试获取变量,找不到的话就会攀爬作用域链层层寻找,找到目标变量则返回,找不到则
undefined
。 - 一个函数能够访问到的上层作用域,在函数创建的时候就已经被确定且保存在函数的
[[scope]]
属性里,和函数拿到哪里去执行没有关系。 - 一个函数调用时的
this
指向,取决于它的调用者,通常有以下几种方式可以改变函数的this
值:对象调用、call
、bind
、apply
。
相关参考
[1]面试官:说说执行上下文吧 (juejin.cn/post/684490...)
[2]一文搞懂执行上下文、VO、AO、Scope、[[scope]]、作用域链、闭包、this (juejin.cn/post/712172...)