深入JavaScript作用域链


作者:Seiya

时间:2019年06月14日


前言


《深入 JavaScript 变量对象》 关于变量对象的描述中,我们已经知道一个执行上下文的数据(变量、函数声明和函数的形参)作为属性存储在变量对象中。同时我们也知道变量对象在每次进入上下文时创建,并填入初始值,值的更新出现在代码执行阶段。


对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)

  • 作用域链(Scope chain)

  • this


这次我们对作用域链进行深入的讨论。



作用域链


定义


作用域链与一个执行上下文相关,变量对象的链用于在标识符解析中变量查找。


简单解释就是,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。


函数的生命周期


函数创建


之前的文章,我们讨论过,在进入上下文时函数声明放到变量/活动(VO/AO)对象中,来看一个例子:

var x = 10;
function foo() {
	var y = 20;
	alert(x + y);
}

foo(); 		// 30

我们看到变量“y”在函数“foo”中定义(意味着它在foo上下文的AO中),但是变量“x”并未在“foo”上下文中定义,相应地,它也不会添加到“foo”的AO中。乍一看,变量“x”相对于函数“foo”根本就不存在。

那么函数“foo”如何访问到变量“x”的?ECMAScript 实现了一种机制,通过函数内部的 [[scope]] 属性来访问一个更高一层上下文的变量对象。


[[scope]] 是所有父变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。

重要:

[[scope]] 在函数创建时被存储--静态(不变的),直至函数销毁。即:函数可以永不调用,但 [[scope]] 属性已经写入,并存储在函数对象中。

注意:

与作用域链对比,[[scope]] 是函数的一个属性而不是上下文。


举个例子:

function foo() {
    function bar() {
        ...
    }
}

函数创建时,各自的 [[scope]] 属性为:

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

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];


函数激活


当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。如下所示;

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

我们用一个稍微复杂的例子描述作用域链的创建过程:

var x = 10;
function foo() {
	var y = 20;
	function bar() {
    var z = 30;
    alert(x +  y + z);
	}
	bar();
}

foo(); // 60

具体执行分析:

  • 第一步:全局上下文的变量对象是:

    globalContext.VO === Global = {
    	x: 10
    	foo: <reference to function>
    };
    

  • 第二步:在“foo”创建时,“foo”的 [[scope]] 属性是:

    foo.[[Scope]] = [
    	globalContext.VO
    ]
    

  • 第三步:在“foo”激活时(进入上下文),“foo”上下文的活动对象是:

    fooContext.AO = {
    	y: 20,
    	bar: <reference to function>
    };
    

  • 第四步:“foo”上下文的作用域链为:

    fooContext.Scope = fooContext.AO + foo.[[Scope]]
    
    fooContext.Scope = [
    	fooContext.AO,
    	globalContext.VO
    ]
    

  • 第五步:内部函数“bar”创建时,其 [[scope]] 为:

    bar.[[Scope]] = [
    	fooContext.AO,
    	globalContext.VO
    ]
    

  • 第六步:在“bar”激活时,“bar”上下文的活动对象为:

    barContext.AO = {
    	z: 30
    }
    

  • 第七步:“bar”上下文的作用域链为:

    barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
    
    barContext.Scope = [
    	barContext.AO,
    	fooContext.AO,
    	globalContext.VO
    ]
    

  • 第八步:此时,对“x”、“y”、“z”的标识符解析如下:

    - "x"
    -- barContext.AO 		// not found
    -- fooContext.AO 		// not found
    -- globalContext.VO 	// found - 10
    
    - "y"
    -- barContext.AO 		// not found
    -- fooContext.AO 		// found - 20
    
    - "z"
    -- barContext.AO 		// found - 30
    
最后更新时间: 2019-7-7 21:55:38