JS作用域链的详解

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

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

在每个对象中进行属性查找的时候,还会使用该对象的原型域链(后续将会讲解原型链)。在一个执行上下文,与其关联的作用域链只会被 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)······以此类推,直到找到所有需要的变量值为止,如图所示。

JS作用域链的详解

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

1) 函数创建

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

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

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

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

2) 函数激活

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

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

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

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

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

函数执行过程如下:
1) f 函数被创建,保存作用域链到内部属性[[scope]]。

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

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

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

3) f 函数并不立刻执行,开始准备工作。准备工作包括以下 3 项:
①复制函数 f 的 [[scope]] 属性,创建作用域链。

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

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

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

③将活动对象压入 f 作用域链顶端。

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

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

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

5) 查找到本地变量 1 的值,然后返回 1 的值。

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

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

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/23141.html

(0)
上一篇 2021年7月20日
下一篇 2021年7月20日

相关推荐

发表回复

登录后才能评论