JavaScript深入理解之作用域链

JavaScript深入理解之作用域链文章配图

写在前面

在文章《JavaScript深入理解之执行上下文》中我们谈到了执行上下文一共有三个属性:变量对象、作用域链、this指针。本篇文章我们将介绍执行上下文第二个重要的属性 —— 作用域链,希望大家建立在对执行上下文和变量对象概念有一定了解的基础上阅读。

定义

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。

在上一篇文章中我们介绍了变量对象里边包含了执行上下文中所有变量和函数的声明,它的作用就是保证代码执行时对变量和函数的正确访问。如果在该变量对象中没有找到对应变量或函数,则会根据执行作用域链向上继续查找,这就是我们今天要介绍的主题。

结构

作用域链本质上是一个指向变量对象的指针列表(在文中我们使用数组表示),它只引用但不实际包含变量对象。作用域链的前端始终都是当前执行上下文的变量对象,如果这个执行上下文属于函数执行上下文,则用活动对象作为变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。

创建过程

一般我们都认为作用域链是在函数定义时就已经创建好的,所以它只和定义时的函数包含关系有关。这样理解作用域链其实看似很清晰,但其实只是片面的理解。因为大家有没有想过一个问题,如果一开始就定义好,和代码执行阶段没有关系的话,那我们是如何来访问那些在代码执行中动态变化的对象的呢?

这其实是一个动态的创建过程,下面我们通过一个例子来看一下作用域链的创建过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var number = 1;

compare(5,10);

function compare(value1,value2){

inner();

if(value1 < value2){
return -1;
} else if( value1 > value2 ) {
return 1;
} else {
return 0;
}

function inner(){
var a = 1;
}
}

全局阶段

我们从程序开始时讲起。首先在执行全局代码前,我们会先创建全局上下文。创建全局上下文的第一步是创建全局变量对象,然后将全局变量对象放入作用域链的顶端(执行上下文中的[[Scope]]属性指向作用域链)。如下图所示(图中省略了其他属性),

配图13-2

此时全局上下文中的[[Scope]]属性可以这样表示

1
2
3
globalContext.[[Scope]] = [
globalContext.VO
];

注意在创建完作用域链后,JavaScript 引擎还做了另一件事,这也是实现作用域链的最关键的一步,它会为变量对象中的所有函数添加一个[[Scope]]属性,而这个属性的值就是我们刚才介绍的全局上下文中的[[Scope]]属性值。如下,

1
2
3
compare.[[Scope]] = [
globalContext.VO
];

函数阶段

全局上下文创建后,开始执行代码,根据代码动态的修改变量对象中的属性,当我们执行到compare(5,10)时,让我们来看一看在函数阶段是如何创建作用域链的。

首先在执行compare(5,10)之前,我们会为函数创建对应的执行上下文。注意重点来了函数上下文首先会复制函数的[[Scope]]属性用来创建作用域链,然后用 arguments 创建活动对象,最后再将活动对象压入作用域链顶端,如下所示,

配图13-3

comapre执行上下文复制[[Scope]]属性创建作用域链后可以这样表示

1
2
3
compareContext = {
Scope: compare.[[Scope]],
}

压入活动对象后作用域链如下

1
2
3
4
5
6
checkScopeContext = {
AO: {
...
},
Scope: [AO, [[Scope]]]
}

在创建好作用域链后,JavaScript 引擎同样会判断函数执行上下文的活动对象中的函数声明,然后为变量对象中的所有函数添加一个[[Scope]]属性,而这个属性的值就是当前函数上下文中的[[Scope]]属性值。如所示

1
2
3
4
inner.[[Scope]] = [
checkScopeContext.AO,
globalContext.VO
];

在代码执行阶段会对函数上下文中属性动态修改,因为作用域链是对变量对象的引用,因此我们可以实时地获取变量对象的最新状态,保证对作用域链查询时能够保证变量的准确性。

接下来对于 inner 函数,在执行到它时会继续这些上面同样的操作来创建作用域链,这就是我为什么说作用域链的创建是一个动态的过程的原因。

小结一下

相信大家通过上面的讲解已经明白了作用域链是如何创建的了,下面我们小结一下。

  1. 全局上下文阶段,创建全局对象。

  2. 将全局对象压入作用域链

  3. 为全局对象中所有函数创建[[Scope]]属性,并将作用域链保存到该属性。(若无函数则跳过此步骤)

  4. 每一个函数上下文阶段,复制函数的[[Scope]]属性,创建作用域链

  5. 创建活动对象,并用 arguments 创建活动对象

  6. 将活动对象压入当前上下文中的作用域链

  7. 为活动对象中所有函数创建[[Scope]]属性,并将作用域链保存到该属性。(若无函数则跳过此步骤)

扩展作用域链

虽然执行上下文的类型总共只有两种———全局和局部(函数),但还是有办法延长作用域链的。这么说是因为有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。有两种情况会发生这种现象,如下:

  • try-catch 语句中的 catch 块

    当 catch 语句执行时,会创建一个新的变量对象,其中包含了被抛出的错误对象的声明,然后将这个变量对象压入当前上下文作用域链中。

  • with 语句

    当 with 语句执行时,会将 with () 中指定的对象压入当前上下文作用域链中。

    1
    2
    3
    4
    5
    6
    7
    function outer(){
    var number = 1;

    with(location){

    }
    }

    此时会将 location 对象添加到当前上下文作用域链的顶端。

写在最后

好啦,作用域链的总结就基本到这里了。作用域链的理解对于 JavaScript 中如闭包等的理解很有帮助,这是一个很基础和重要的知识点,理解后的确有一种豁然开朗的感觉。

本篇文章纯属于个人的学习总结,如果文章中出现错误或不严谨的地方,希望大家能够指出,谢谢!

坚持原创技术分享,您的支持将鼓励我继续创作!