JS执行上下文之作用域链

avatar
Ctrl+C、V工程师 @豌豆公主

之前说过,执行上下文有三个重点:

  1. 变量对象 VO
  2. 作用域链 Scope Chain
  3. this

这篇文章主要记录的是 作用域链

什么是作用域链

  • 在一个执行上下文中,有属于自己的变量对象,可很多时候我们的执行上下文并不是那么简单的,会出现执行上下文嵌套的情况。如果在当前的执行上下文没有找到变量,就会向外层执行上下文中的变量对象中查找,一直找到全局上下文
  • 这种变量对象嵌套的变量对象链表,我们可以称之为 作用域链

作用域链有什么用

保证对执行上下文中的有权访问的所有变量和函数的有序访问。这句话有点绕口,大家可以简单的认为作用域链保证了上下文嵌套中,变量访问的顺序。

从一个函数开始分析作用域链

这个是《高程》中的一个例子。这个过程会涉及之前提到的词法分析作用域执行上下文栈变量对象

var color = 'blue';
function changeColor() {
  var anotherColor = 'red';
  
  function swapColors() {
    var tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;
  }
  swapColors();
}
changeColor();
  1. changeColor函数声明。因为它的声明是在全局上下文中,所以它所继承的父级作用域链是全局上下文的变量对象,也就是 VO。 之前我们也说过,在JS的作用域是静态作用域,因此它的作用域是在声明的时候被决定的。这个时候作用域就会被保存在 [[scope]]属性中。

    changeColor.[[scope]] = [globalContext.VO];
    
  2. 接下来遇到一个函数被执行,也就是changeColor();执行上下文是在函数被执行的时候创建的,并且需要进入到执行上下文栈中(ECStack)

    ECStack.push("changeColorContext");
    
  3. changeColor正式被JS引擎执行前,为了方便理解,我认为他还有一个预编译阶段。在这个阶段中,我们会把之前保存在changeColor.[[scope]]中的作用域放进changeColorContext中。

    changeColorContext = {
      scope: changeColor.[[scope]]
    }
    
  4. 进入changeColor的执行上下文,创建活动变量AO。我们在变量对象中说到,AO 大体上会分为两个阶段:声明阶段、执行阶段。因此先进行声明阶段的工作。

    changeColorContext = {
      scope: changeColor.[[scope]],
      AO: {
        arguments:{
          length: 0
        }, // 因为这个函数中没有形参
        anotherColor: undefined,
        swapColors: reference to function swapColor()
      }
    }
    
  5. AO进行执行阶段前,先会合并作用域,让之后的改值有变量可依。

    changeColorContext = {
      AO: {
        arguments:{
          length: 0
        }, // 因为这个函数中没有形参
        anotherColor: undefined,
        swapColors: reference to function swapColor()
      },
      scope: [AO, changeColor.[[scope]]],
    }
    
  6. AO进入执行阶段。修改各个变量的值。

    changeColorContext = {
      AO: {
        arguments:{
          length: 0
        }, // 因为这个函数中没有形参
        anotherColor: 'red',
        swapColors: reference to function swapColor()
      },
      scope: [AO, changeColor.[[scope]]],
    }
    
  7. 进入changeColor的执行上下文之后,有一个函数声明swapColors同理,进行作用域的创建。

    swapColors.[[scope]] = [changeColorContext.scope];
    
  8. 执行swapColors前先 push 到 ECStack中。

    ECStack.push("swapColorsContext");
    // 此时 ECStack 中已经有两个执行上下文。
    
  9. 初始化 swapColorsContext的作用域链。

    swapColorsContext = {
      scope: swapColors.[[scope]]
    }
    
  10. 初始化 swapColorsContextAO

    swapColorsContext = {
      scope: swapColors.[[scope]],
      AO: {
        arguments:{
          length: 0
        }, // 因为这个函数中没有形参
        tempColor: undefined
      }
    }
    
  11. 合并swapColorsContextscope

    swapColorsContext = {
      scope: [AO, swapColors.[[scope]]],
      AO: {
        arguments:{
          length: 0
        }, // 因为这个函数中没有形参
        tempColor: undefined
      }
    }
    
  12. AO进入执行阶段。修改各个变量的值。

    swapColorsContext = {
      scope: [AO, swapColors.[[scope]]],
      AO: {
        arguments:{
          length: 0
        }, // 因为这个函数中没有形参
        tempColor: 'red'
      }
    }
    
  13. swapColors执行结束,出栈。

    ECStack.pop();
    
  14. changeColor执行结束,出栈。

    ECStack.pop();
    
  15. 程序终于结束了!!!写的我好累。

总结

  1. 作用域是在函数声明阶段创建的。
  2. 执行上下文每次创建都会进入执行上下文栈。
  3. 执行上下文中会先初始化活动变量AO再合并作用域链最后才修改AO中的值