作用域链

课后整理 2020-12-10

JavaScript作用域属于静态概念,根据词法结构来确定,而不是根据执行来确定。作用域链是JavaScript提供的一套解决标识符的访问机制。JavaScript规定每一个作用域都有一个与之相关联的作用域链。

作用域链用来在函数执行时,求出标识符的值。该链中包含多个对象,在对标识符进行求值的过程中,会从链首的对象开始,然后依次查找后面的对象,直到在某个对象中找到与标识符名称相同的属性。如果在作用域链的顶端(全局对象)中仍然没有找到同名的属性,则返回undefined的属性值。

【注意】

在每个对象中进行属性查找的时候,还会使用该对象的原型域链(可参考第10章内容)。在一个执行上下文中,与其关联的作用域链只会被with语句和catch 子句影响。

【示例1】在下面示例中,通过多层嵌套函数设计一个作用域链,在最内层函数中可以逐级访问外层函数的私有变量。

var a = 1;                                                 //全局变量 
(function(){
    var b = 2;                                          //第1层局部变量 
    (function(){
        var c = 3;                                   //第2层局部变量 
        (function(){
            var d = 4;                            //第3层局部变量 
            console.log(a+b+c+d);          //返回10
        })()                                            //直接调用函数 
    })()                                                   //直接调用函数 
})()                                                          //直接调用函数 

在上面代码中,JavaScript引擎首先在最内层活动对象中查询属性a、b、c和d,其中只找到了属性d,并获得它的值(4),然后沿着作用域链,在上一层活动对象中继续查找属性a、b和c,其中找到了属性c,获得它的值(3),依此类推,直到找到所有需要的变量值为止。

下面结合一个示例,通过函数的创建和激活两个阶段来介绍作用域链的创建过程。

函数的作用域在函数定义的时候就已经确定。每个函数都有一个内部属性[[scope]],当函数创建的时候,[[scope]]保存所有父变量对象的引用,[[scope]]就是一个层级链。注意,[[scope]]并不代表完整的作用域链。例如:

function f1() {
    function f2() {
        //......
    }
}

在函数创建时,每个函数的[[scope]]如下,其中globalContext表示全局上下文,VO表示变量对象,f1Context表示函数f1的上下文,AO表示活动对象。

f1.[[scope]] = [
    globalContext.VO
];
f2.[[scope]] = [
    f1Context.AO,
    globalContext.VO
];

当函数激活时,进入函数上下文,创建VO/AO后,就会将活动对象添加到作用链的前端。这时候如果命名执行上下文的作用域链为Scope,则可以表示为:

Scope = [AO].concat([[Scope]]);

至此,作用域链创建完毕。

【示例2】下面示例结合变量对象和执行上下文栈,总结函数执行上下文中作用域链和变量对象的创建过程。

var g = "global scope";                              //全局变量 
function f(){                                             //声明函数 
    var l = 'local scope';                            //私有变量 
    return l;                                             //返回私有变量 
}
f();                                                          //调用函数 

【执行过程】

第1步,f函数被创建,保存作用域链到内部属性[[scope]]。

f.[[scope]] = [                                           //当前函数的作用域链 
    globalContext.VO                              //全局上下文的变量对象 
];

第2步,执行f函数,创建f函数的执行上下文,f函数的执行上下文被压入执行上下文栈。

ECStack = [                                              //执行上下文栈 
    fContext,                                          //函数的执行上下文 
    globalContext                                    //全局上下文 
];

第3步,f函数并不立刻执行,开始做准备工作。准备工作包括三项:

第一小步:复制函数f的[[scope]]属性,创建作用域链。

fContext = {                                             //函数的执行上下文 
    Scope: f.[[scope]],                             //把函数的作用域链添加到函数的执行上下文 
}

第二小步:使用arguments创建活动对象,然后初始化活动对象,加入形参、函数声明、变量声明。

fContext = {                                             //函数的执行上下文 
    AO: {                                               //函数的活动对象 
        arguments: {                               //为活动对象添加arguments
            length: 0
        },
        l: undefined                                //创建本地变量 
    }
}

第三小步:将活动对象压入f 作用域链顶端。

fContext = {                                             //函数的执行上下文 
    AO: {                                               //活动对象 
        arguments: {                               //参数集合 
            length: 0
        },
        l: undefined                                //本地变量 
    },
    Scope: [AO, [[Scope]]]                       //作用域链 
}

第4步,准备工作做完,开始执行函数,随着函数的执行,修改AO的属性值。

fContext = {                                             //函数的执行上下文 
    AO: {                                               //活动对象 
        arguments: {                               //参数集合 
            length: 0
        },
        l: 'local scope'                              //初始化本地变量 
    },
    Scope: [AO, [[Scope]]]                      //作用域链 
}

第5步,查找到本地变量l的值,然后返回l的值。

第6步,函数执行完毕,函数上下文从执行上下文栈中弹出。

ECStack = [                                              //执行上下文栈 
    globalContext                                    //全局上下文 
];