JavaScript ---- 作用域链(scope chain)

91 阅读4分钟

引文

在前面的文章里讨论了执行上下文,对于每个执行上下文都有三个重要的属性:

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

本篇文章重点讲讲作用域链相关内容

读前需要了解的概念

从ES6(ECMAScript 2015)开始,引入了更明确的“词法环境”(Lexical Environment)概念,词法环境由两部分组成:

  • 环境记录(Environment Record)和对外部环境的引用。环境记录存储了变量、函数等绑定
  • 外部环境的引用:形成了作用域链,用于变量查找

什么是作用域链?

作用域链(Scope Chain)决定了引擎如何在多个嵌套的作用域中查找变量,当Javascript需要查找一个变量时,会从当前执行上下文的词法环境(Lexical Environment)的环境记录(Environment Record)开始查找,如果在当前环境记录中没有找到,就会沿着作用域链向外层词法环境(即父级执行上下文)的环境记录中继续查找,直至全局执行上下文或找到变量为止,这样的一条为了查找变量形成的链式结构就是 作用域链 

函数创建时

由于JavaScript采用的是词法作用域(lexical scoping)或静态作用域,函数的作用域在定义函数的时候就已经确定了,就是在写代码时函数和变量声明的位置决定了作用域的规则,而不是在代码运行时动态决定。

当函数被创建时,它的内部会有一个内部属性[[Scope]],这个属性是一个包含了函数所有上级作用域的链表。这个链表从当前函数的词法环境开始,一直延伸到全局作用域。每当函数被调用时,一个新的词法环境会被创建并链接到当前的作用域链顶端。

举个例子:


function outerFn() {
    function innerFn() {
        ...
    }
}
// 函数创建时,它们的[[scope]]分别为

outerFn.[[scope]] = [
  globalContext.Vo,
];

innerFn.[[scope]] = [
  outerFnContext.Ao,
  globalContext.Vo,
]

函数调用时

上文中提到“每当函数被调用时,一个新的词法环境会被创建并链接到当前的作用域链顶端”也就是说:

每当一个函数被调用,JavaScript引擎会为这个函数实例创建一个新的词法环境,这个环境包含了函数内部的变量和函数声明。这个新环境会被添加到当前作用域链的最前端,使得函数内部不仅可以访问自己的局部变量,还可以通过作用域链访问到外部作用域中的变量,此时作用域链才真正创建完毕。当函数执行完毕后,这个为函数创建的词法环境通常会被销毁(除非涉及闭包等特殊情况),以释放资源。

// 此时执行上下文的作用域链Scope
Scope = [Ao].concat([[Scope]])

总体流程

在下面的例子中,我们结合之前讲过的执行上下文栈和变量对象,总结一下函数执行上下文中作用域链和变量对象的创建过程:

var globalV = 'global';
function outerFn () {
  var outerV = 'outer';
  return outerV;
}
outerFn();

1、outerFn函数被创建,保存上级作用域链到内部属性[[Scope]]

outerFn.[[Scope]] = [
  globalContext.Vo,
]

2、执行outerFn函数,创建执行上下文,将该执行上下文压入执行上下文栈

ECStack = [
  outerFnContext,
  globalContext,
]

3、outerFn函数执行前会先做准备工作,即初始化作用域链:将当前执行上下文的外部作用域(函数的[[Scope]]属性指向的作用域链)拷贝,并在其前端添加当前执行上下文的活动对象。这时的活动对象中已经包含了函数参数、arguments对象以及函数声明和变量声明,但它们都还未被赋予实际的值或执行函数体内的初始化代码

// 1 拷贝外部作用域
outerFnContext = {
  Scope: outerFn.[[scope]],
}
// 初始化活动对象
outerFnContext = {
  AO: {
        arguments: {
            length: 0
        },
        outerV: undefined
    }
}

// 添加活动对象到Scope前端
outerFnContext = {
  Scope: AO.concat(outerFn.[[scope]]),
}

4、做完准备工作以后开始执行函数,修改AO的值

outerFnContext = {
  AO: {
        arguments: {
            length: 0
        },
        outerV: 'outer'
    },
  Scope: [AO, outerFn.[[scope]]]
}

5、查找到outerV的值,返回后函数执行完毕,从执行上下文栈中弹出outerFnContext

ECStack = [
  globalContext,
]