深入Js作用域:作用域链

前言

之前在执行上下文那篇博客中提到,每个执行上下文都有三个重要属性:

变量对象(Variable Object,VO)
Scope(作用域链,Scope Chain)
this

我们这里就来重点讲一下作用域链。

作用域链

Scope Chain,可以将其理解为表示 执行上下文 与 对应VO 之间联系的一个对象表或链表

在 Js 最顶层代码中(也就是全局代码),作用域链上有一个全局对象。
在不包含嵌套的函数体内,作用域链上有两个 VO,分别是函数执行上下文 VO、全局对象。
在一个嵌套函数体内,作用域链上至少有三个 VO。

函数 [[scope]] 属性

函数对象具有仅供 Js 引擎内部使用,但不能通过代码访问的一系列内部属性。其中一个就是 [[scope]] 属性。

[[scope]] 是所有父变量对象的层级链,处于当前函数上下文之上,在函数创建时被存储(静态,不变的),直至函数销毁。即:函数可以永不调用,但 [[scope]] 属性已经写入,并存储在函数对象中。

很多书提到,在 [[scope]] 中存放作用域链,其实最易理解不易混淆的应该是父变量对象的层级链。

注:标识符解析就是沿着作用域链一级一级地查找变量对象中变量的过程,查找顺序是从作用域链前端开始,逐级向后。

这种联合和标识符解析过程与函数的生命周期相关。

函数生命周期

函数的的生命周期分为创建和激活阶段(调用时),我们分别进行分析。

函数创建

不存在嵌套:

Fun.[[scope]] = [
    globalContext.VO // === Global
]

存在嵌套:

Fun1.[[scope]] = [
    Fun2Context.AO, 
    ...,
    globalContext.VO // === Global
]

函数激活

函数上下文的作用域链是在函数调用时创建的,包含 VO 和这个函数内部的 [[scope]] 属性。

ActiveExecutionContext = {
    VO: {...}, // or AO
    this: thisValue,
    Scope: [ // Scope Chain
      // 所有变量对象的列表
      // 用于标识符解析
    ]
};

其scope定义如下:

Scope = AO + [[Scope]] 

由此可知:与作用域链对比,[[scope]] 是函数的一个属性而非上下文,即 [[scope]] 并不代表完整的作用域链。
下面通过一个例子来看看。

样例

创建时:

function add(a, b) {
    var c = a + b;
    return c;
}

此时:

add.[[Scope]] = [
  globalContext.VO // === Global
];    

调用时:

var total = add(1, 2);

此时:

addContext.Scope = AO.concat(add.[[Scope]])

addContext = {
    AO: 略,
    Scope: [AO, globalContext.VO]
    this: undefined
}

标识符解析

对应图解过程:

知识串联

我在此处将 执行上下文(EC)、变量对象(VO)、作用域链 进行串联,融合到一个例子中进行流程梳理。

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

1、执行全局代码,创建 全局EC,全局EC 被压入 ECS

ECStack = [
    globalContext
];

2、EC 初始化

globalContext = {
    VO: [global],
    Scope: [globalContext.VO],
    this: globalContext.VO
}

初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性 [[scope]]

checkscope.[[scope]] = [
    globalContext.VO
];

3、执行 checkscope 函数,创建 checkscope 函数EC,checkscope 函数EC 被压入 ECS

ECStack = [
    checkscopeContext,
    globalContext
];

4、checkscope 函数EC 初始化:

复制函数 [[scope]] 属性创建作用域链,
用 arguments 创建活动对象,
初始化活动对象,即加入形参、函数声明、变量声明,
将活动对象压入 checkscope 作用域链顶端。
同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]

checkscopeContext = {
    AO: {
      arguments: {
          length: 0
      },
      scope: undefined,
      f: reference to function f(){}
    },
    Scope: [AO, globalContext.VO],
    this: undefined
}

5、执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈

ECStack = [
    fContext,
    checkscopeContext,
    globalContext
];

6、f 函数执行上下文初始化, 以下跟第 4 步相同:

复制函数 [[scope]] 属性创建作用域链
用 arguments 创建活动对象
初始化活动对象,即加入形参、函数声明、变量声明
将活动对象压入 f 作用域链顶端

fContext = {
    AO: {
        arguments: {
            length: 0
        }
    },
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
    this: undefined
}

7、f 函数执行,沿着作用域链查找 scope 值,返回 scope 值

8、f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

ECStack = [
    checkscopeContext,
    globalContext
];

9、checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出

ECStack = [
    globalContext
];

小结

看到这里,关于 Js 作用域已经介绍了大半,最后还剩下闭包这个概念,我会在下一篇文章给大家进行讲解。