JavaScript运行机制解析(四)作用域与作用域链

82 阅读4分钟

前言

在第一节中,我们介绍了可执行代码以及执行上下文的相关知识,对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object, VO
  • 作用域、作用域链(Scope chain
  • this指向问题

这节介绍作用域与作用域链,也是面试重中之重。

一、作用域

JavaScript采用的是词法作用域,即静态作用域。函数的作用域在函数定义的时候就决定了。在之前的章节中,我们提到了执行上下文,函数的执行上下文是在函数调用的时候生成的。

所以两者的区别在于作用域:静态的(写完代码就确定),而执行上下文:动态的(调用的时候才产生)。

二、作用域链

当执行到函数内部(ES6中描述为块级作用域中),当查找一个变量的时候,会先从当前上下文的变量对象中进行查找,如果没有找到,就会从父级执行上下文中的变量对象中进行查找,一直找到顶级的全局上下文中。这种查找路径形成的关系称为作用域链。

关于这个查找过程,下面结合第2节谈到的变量对象进行演示。

三、函数内部属性[[scope]]

首先,JavaScript在运行过程中,函数首先会被创建,然后才能被执行。函数中有一个内部属性[[scope]],当函数创建的时候,会保存所有父变量对象到这个[[scope]]中,注意,它不会保存子级的变量对象,所以[[scope]]并不代表完整的作用域链

例如:

var a = 100;
function f1() {
  function f2() {}
}

函数创建的时候,f1f2各自[[scope]]情况如下:

f1.[[scope]] = [
  globalContext.VO
]
​
f2.[[scope]] = [
  f1Context.VO
  globalContext.VO
]

函数激活的时候,创建函数上下文,创建VO/AO将活动对象添加到作用域链的前端

此时,当前函数的作用域链为:

Scope = [
  AO,
  [[scope]]
]

注意:问题来了,为什么要把AO添加到作用域链呢?

我们通过一段代码来演示一下函数创建到执行的过程中,变量对象和作用域链的生成过程是如何的。

四、生成过程

var a = 100;
function f1() {
  var b = 200;
  function f2(c, d) {
    console.log(a, b)
    console.log(c, d)
  }
  f2();
}
f1();

执行过程如下:

  1. 初始化全局上下文,f1函数被创建,保存父作用域链到内部属性[[scope]],初始全局VO

    // 1. 初始化全局上下文
    window.[[scope]] = globalContext.VO
    globalContext = {
      VO: {
        a: undefined
      }
    }
    // 2. 创建函数作用域链
    f1.[[scope]] = [
      globalContext.VO  
    ]
    
  2. 调用f1函数,进行前期准备工作,创建其函数执行上下文,进行压栈操作,之后复制函数自己的[[scope]]属性创建当前函数作用域链到自己的上下文中

    f1Context = {
      Scope: f1.[[scope]]
    }
    
  3. 创建A0对象,初始化A0对象,如加入形参、函数声明、变量声明。

    f1Context = {
      AO: {
        arguments: {
          length: 0
        },
        f2: f2Function() {}
        b: undefined,
      },
      Scope: f1.[[scope]]
    }
    
  4. 将活动对象压入f1作用域链顶端

    f1Context = {
      AO: {
        arguments: {
          length: 0
        },
        f2: f2Function() {}
        b: undefined,
      },
      Scope: [
        AO,
        globalContext.VO
      ]
    }
    
  5. 准备工作完成,开始执行函数代码,修改AO对象属性

    f1Context = {
      AO: {
        arguments: {
          length: 0
        },
        f2: f2Function() {}
        b: 200,
      },
      Scope: [
        AO,
        globalContext.VO
      ]
    }
    
  6. 注意,因为f1这里内部还有函数声明f2,过程同理,在进入f1的时候,也会创建f2函数,创建函数作用域链,创建执行上下文,进行压栈操作,初始化AO对象,运行过程中进行赋值AO对象属性,最后打印的时候根据当前函数的Scope。回答前面问题,为什么要把AO添加到作用域链中,即查找顺序是先从作用域链最前端查找,即当前AO查找是否存在,没有的话再查找上一级[[scope]],如此进行递归查找。

    f2.[[scope]] = [
        f1Context.VO,
        globalContext.VO  
    ]
    
    f2Context = {
      AO: {
        arguments: {
          0: c,
          1: d
          length: 2
        },
      },
      Scope: [
        AO,
        f1Context.VO,
        globalContext.VO
      ]
    }
    

五、总结

本节介绍了作用域与作用域链的关系,同时作用域链形成的原因,再介绍JS在运行过程中是如何编译函数内部的代码,最后通过案例演示了作用域链形成和查找的全过程,在遇到此类问题的时候,可以直接套这个方式进行拆解分析。