定义
执行上下文栈(Execution context stack,ECS)
执行上下文 (Execution context, EC)
每次当控制器转到ECMAScript可执行代码的时候,即会进入到一个执行上下文。
活动的执行上下文组在逻辑上组成一个堆栈,堆栈在EC类型进入和退出上下文的时候被修改(推入或弹出)。
可执行代码
可执行代码(executable code)的类型有三种,全局代码、函数代码、eval
代码
所以也就存在三种上下文,全局执行上下文、函数执行上下文、eval
执行上下文
执行上下文
对于每个执行上下文,都有三个重要属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
执行上下文栈底部永远都是全局上下文(global context),而顶部就是当前(活动的)执行上下文。
当调用一个函数时,开始执行前会创建一个该函数的执行上下文,并将该执行上下文压入执行栈,当函数结束后再将该函数的执行上下文从执行栈中推出。
function fn1() {
fn2()
}
function fn2() {
fn3()
}
function fn3() {}
fn1()
// 以上的处理过程
// 伪代码
// 在开始执行时会创建全局执行上下文
ECS = [ globalContext ]
// 执行 fn1
ECS = [ fn1Context, globalContext ]
// fn1 里调用了 fn2,执行 fn2
ECS = [ fn2Context, fn1Context, globalContext ]
// fn2 里调用了 fn3,执行 fn3
ECS = [ fn3Context, fn2Context, fn1Context, globalContext ]
// 当 fn3执行完后,推出 fn3 的执行上下文
ECS = [ fn2Context, fn1Context, globalContext ]
// 继续执行 fn2,fn2 执行完后推出栈
ECS = [ fn1Context, globalContext ]
// 继续执行 fn1,fn1执行完后推出栈
ECS = [ globalContext ]
// 至些,执行完,全局执行上下文一个保留
执行上下文生命周期:
共有三大阶段:
- 创建阶段
- 执行阶段
- 回收阶段
- 创建阶段
- 初始化变量对象VO
- 处理函数的形参
- 用
arguments
初始化变量对象属性,arguments
属性的值是Arguments
对象。 - 没有实参,对应的形参的值是
undefined
。
- 用
- 函数声明
- 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建。
- 如变量对象已经存在相同名称的属性,则完全替换这个属性。
- 变量声明
- 由名称和对应值(undefined)组成一个变量对象的属性被创建。
- 如变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
- 处理函数的形参
- 建立作用域链
functionContext.scope = function.VO + function.[[scope]]
- 先取到在词法阶段定义的词法作用域,存于函数的属性
[[Scope]]
中,再复制到functionContext.scope
中。 - 并将当前
VO
对象放在scope
属性前端。
- 确定this的指向
this
的值是在执行的时候才能确认,定义的时候不能确认。- 一般由调用者提供,谁调用
this
一般指向谁。 call
、apply
和bind
:this
是第一个参数。- 构造函数中,被构造函数是
this
。 - 箭头函数没有
this
, 由父上下文提供。
- 初始化变量对象VO
- 执行阶段
- 开始执行代码。
- 标识符赋值和引用,执行其它代码。
- 此时变量对象 AO 变成活动对象 VO。
- 将生成的当前执行上下文推入执行上下文栈(ECS)
- 回收阶段
- 将当前执行上下文弹出执行栈
- 销毁当前上下文
- 闭包的情况另说
基础情况示例代码
function fn(a, b, c){
var m = 10;
var n = 20
var x = 30
var y = 40
p = 50
var n = 25
function sum(){
return a + b + c + m + n
}
var out = function() {}
function x() {}
}
fn(1, 2)
// 函数 fn 在创建时的词法作用域是
fn.[[Scope]] = [
globalContext.VO
]
// 1. 执行函数 fn 代码前先创建 fn 的执行上下文
fnContext = {}
// 2. 使用 arguments 初始化 VO
fnContext.VO = {
arguments: {
0: 1,
1: 2,
length: 2
}
}
// 3. 对函数和变量的提升,继续初始化VO
fnContext.VO = {
arguments: {
0: 1,
1: 2,
length:2
},
a: 1,
b: 2,
c: undefined,
m: undefined,
n: undefined,
x: <reference function x>,
y: undefined,
sum: <reference function sum>,
out: undefined
}
// 4. 对执行上下文进行作用域建立
fnContext = {
VO: {},
Scope: [[Scope]]
}
// 5. 对执行上下文进行作用域建立
fnContext = {
VO: {},
scopeChain: [[Scope]]
}
// 6. 将自身 VO 推入作用域
fnContext = {
VO: {},
scopeChain: [ VO, [[Scope]] ]
}
// 等价于
fnContext = {
VO: {},
scopeChain: [VO(fn), VO(global)]
}
// 7. 确认 this 指向,当前是全局 window 调用
fnContext = {
VO: {},
scopeChain: [VO(fn), VO(global)],
this: window
}
// 8. 开始执行代码,将当前执行上下文推入执行栈
ECStack = [
fnContext,
globalContext,
]
// 9. 执行代码过程中修改 VO
fnContext = {
VO: {
arguments: {
0: 1,
1: 2,
length:2
},
a: 1,
b: 2,
c: undefined,
m: 10,
n: 25,
x: <reference function x>,
y: 40,
sum: <reference function sum>,
out: <reference function out>,
},
scopeChain: [VO(fn), VO(global)],
this: window
}
// 10. 执行结束,将 fnContext 弹出执行栈,等待销毁
ECStack = [
globalContext
]
闭包
当以上过程遇到闭包时,执行顺序不变,执行上下文也会在执行栈中弹出,被销毁。但依然能调用上级函数的变量,因为当上级标识符被引用时,上级函数的AO依然会在内存中,不会被销毁,所以当前函数依然能从函数作用域链中找到该变量。
以同上示例代码为例:
function fn(a, b, c){
// ...
function sum(){
return a + b + c + m + n
}
return sum
}
fn(1, 2)()
执行过程继续如上,接着如下:
11. 当 fn
返回 sum
且继续执行时,执行栈情况如下
ECStack = [
sumContext,
globalContext
]
- 在
sum
中没有变量a
,就需要从上级函数中找,此时执行栈中上级函数执行上下文已经被弹出被销毁,但上级函数的活动对象VO不会被销毁,在自身的执行上下文中的ScopeChain
中保存有上级函数VO
sumContext = {
VO: {},
ScopeChain: [sumContext.VO, fnContext.VO, globalContext.VO]
}
- 这样就可以找到对应的变量,然后计算返回结果,执行结束。
- 弹出
sumContext
,销毁sum
的执行上下文,再继续销毁父级的执行上下文(fnContext)