定义

执行上下文栈(Execution context stack,ECS)

执行上下文 (Execution context, EC)

每次当控制器转到ECMAScript可执行代码的时候,即会进入到一个执行上下文。

活动的执行上下文组在逻辑上组成一个堆栈,堆栈在EC类型进入和退出上下文的时候被修改(推入或弹出)。

可执行代码

可执行代码(executable code)的类型有三种,全局代码、函数代码、eval 代码

所以也就存在三种上下文,全局执行上下文、函数执行上下文、eval 执行上下文

执行上下文

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

执行上下文栈底部永远都是全局上下文(global context),而顶部就是当前(活动的)执行上下文。

当调用一个函数时,开始执行前会创建一个该函数的执行上下文,并将该执行上下文压入执行栈,当函数结束后再将该函数的执行上下文从执行栈中推出。

  1. function fn1() {
  2. fn2()
  3. }
  4. function fn2() {
  5. fn3()
  6. }
  7. function fn3() {}
  8. fn1()
  9. // 以上的处理过程
  10. // 伪代码
  11. // 在开始执行时会创建全局执行上下文
  12. ECS = [ globalContext ]
  13. // 执行 fn1
  14. ECS = [ fn1Context, globalContext ]
  15. // fn1 里调用了 fn2,执行 fn2
  16. ECS = [ fn2Context, fn1Context, globalContext ]
  17. // fn2 里调用了 fn3,执行 fn3
  18. ECS = [ fn3Context, fn2Context, fn1Context, globalContext ]
  19. // 当 fn3执行完后,推出 fn3 的执行上下文
  20. ECS = [ fn2Context, fn1Context, globalContext ]
  21. // 继续执行 fn2,fn2 执行完后推出栈
  22. ECS = [ fn1Context, globalContext ]
  23. // 继续执行 fn1,fn1执行完后推出栈
  24. ECS = [ globalContext ]
  25. // 至些,执行完,全局执行上下文一个保留

执行上下文生命周期:

共有三大阶段:

  • 创建阶段
  • 执行阶段
  • 回收阶段
  1. 创建阶段
    1. 初始化变量对象VO
      1. 处理函数的形参
        1. arguments 初始化变量对象属性,arguments 属性的值是 Arguments 对象。
        2. 没有实参,对应的形参的值是 undefined
      2. 函数声明
        1. 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建。
        2. 如变量对象已经存在相同名称的属性,则完全替换这个属性。
      3. 变量声明
        1. 由名称和对应值(undefined)组成一个变量对象的属性被创建。
        2. 如变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
    2. 建立作用域链
      1. functionContext.scope = function.VO + function.[[scope]]
      2. 先取到在词法阶段定义的词法作用域,存于函数的属性 [[Scope]] 中,再复制到 functionContext.scope 中。
      3. 并将当前 VO 对象放在 scope 属性前端。
    3. 确定this的指向
      1. this 的值是在执行的时候才能确认,定义的时候不能确认。
      2. 一般由调用者提供,谁调用 this 一般指向谁。
      3. callapplybindthis 是第一个参数。
      4. 构造函数中,被构造函数是 this
      5. 箭头函数没有 this, 由父上下文提供。
  2. 执行阶段
    1. 开始执行代码。
    2. 标识符赋值和引用,执行其它代码。
    3. 此时变量对象 AO 变成活动对象 VO。
    4. 将生成的当前执行上下文推入执行上下文栈(ECS)
  3. 回收阶段
    1. 将当前执行上下文弹出执行栈
    2. 销毁当前上下文
    3. 闭包的情况另说

基础情况示例代码

  1. function fn(a, b, c){
  2. var m = 10;
  3. var n = 20
  4. var x = 30
  5. var y = 40
  6. p = 50
  7. var n = 25
  8. function sum(){
  9. return a + b + c + m + n
  10. }
  11. var out = function() {}
  12. function x() {}
  13. }
  14. fn(1, 2)
  15. // 函数 fn 在创建时的词法作用域是
  16. fn.[[Scope]] = [
  17. globalContext.VO
  18. ]
  19. // 1. 执行函数 fn 代码前先创建 fn 的执行上下文
  20. fnContext = {}
  21. // 2. 使用 arguments 初始化 VO
  22. fnContext.VO = {
  23. arguments: {
  24. 0: 1,
  25. 1: 2,
  26. length: 2
  27. }
  28. }
  29. // 3. 对函数和变量的提升,继续初始化VO
  30. fnContext.VO = {
  31. arguments: {
  32. 0: 1,
  33. 1: 2,
  34. length2
  35. },
  36. a: 1,
  37. b: 2,
  38. c: undefined,
  39. m: undefined,
  40. n: undefined,
  41. x: <reference function x>,
  42. y: undefined,
  43. sum: <reference function sum>,
  44. out: undefined
  45. }
  46. // 4. 对执行上下文进行作用域建立
  47. fnContext = {
  48. VO: {},
  49. Scope: [[Scope]]
  50. }
  51. // 5. 对执行上下文进行作用域建立
  52. fnContext = {
  53. VO: {},
  54. scopeChain: [[Scope]]
  55. }
  56. // 6. 将自身 VO 推入作用域
  57. fnContext = {
  58. VO: {},
  59. scopeChain: [ VO, [[Scope]] ]
  60. }
  61. // 等价于
  62. fnContext = {
  63. VO: {},
  64. scopeChain: [VO(fn), VO(global)]
  65. }
  66. // 7. 确认 this 指向,当前是全局 window 调用
  67. fnContext = {
  68. VO: {},
  69. scopeChain: [VO(fn), VO(global)],
  70. this: window
  71. }
  72. // 8. 开始执行代码,将当前执行上下文推入执行栈
  73. ECStack = [
  74. fnContext,
  75. globalContext,
  76. ]
  77. // 9. 执行代码过程中修改 VO
  78. fnContext = {
  79. VO: {
  80. arguments: {
  81. 0: 1,
  82. 1: 2,
  83. length2
  84. },
  85. a: 1,
  86. b: 2,
  87. c: undefined,
  88. m: 10,
  89. n: 25,
  90. x: <reference function x>,
  91. y: 40,
  92. sum: <reference function sum>,
  93. out: <reference function out>,
  94. },
  95. scopeChain: [VO(fn), VO(global)],
  96. this: window
  97. }
  98. // 10. 执行结束,将 fnContext 弹出执行栈,等待销毁
  99. ECStack = [
  100. globalContext
  101. ]

闭包

当以上过程遇到闭包时,执行顺序不变,执行上下文也会在执行栈中弹出,被销毁。但依然能调用上级函数的变量,因为当上级标识符被引用时,上级函数的AO依然会在内存中,不会被销毁,所以当前函数依然能从函数作用域链中找到该变量。

以同上示例代码为例:

  1. function fn(a, b, c){
  2. // ...
  3. function sum(){
  4. return a + b + c + m + n
  5. }
  6. return sum
  7. }
  8. fn(1, 2)()

执行过程继续如上,接着如下:
11. 当 fn 返回 sum 且继续执行时,执行栈情况如下

  1. ECStack = [
  2. sumContext,
  3. globalContext
  4. ]
  1. sum 中没有变量 a,就需要从上级函数中找,此时执行栈中上级函数执行上下文已经被弹出被销毁,但上级函数的活动对象VO不会被销毁,在自身的执行上下文中的 ScopeChain 中保存有上级函数VO
  1. sumContext = {
  2. VO: {},
  3. ScopeChain: [sumContext.VO, fnContext.VO, globalContext.VO]
  4. }
  1. 这样就可以找到对应的变量,然后计算返回结果,执行结束。
  2. 弹出 sumContext,销毁 sum 的执行上下文,再继续销毁父级的执行上下文(fnContext)