定义
执行上下文栈(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 ]// 执行 fn1ECS = [ fn1Context, globalContext ]// fn1 里调用了 fn2,执行 fn2ECS = [ fn2Context, fn1Context, globalContext ]// fn2 里调用了 fn3,执行 fn3ECS = [ 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 = 20var x = 30var y = 40p = 50var n = 25function 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 初始化 VOfnContext.VO = {arguments: {0: 1,1: 2,length: 2}}// 3. 对函数和变量的提升,继续初始化VOfnContext.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. 执行代码过程中修改 VOfnContext = {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)
