您现在的位置是:首页 > 文章详情

JavaScript 作用域链 难不难?

日期:2018-05-31点击:639

介绍

变量对象中已经介绍过,执行上下文(变量,函数声明和函数形式参数)的数据被存储为变量对象的属性

此外,我们知道每次进入上下文时都会创建变量对象并填充初始值,并且它的更新发生在代码执行阶段

举个栗子

function test(a, b) { console.log(c); // function c() {} var c = 10; function c() {}; console.log(c); // 10 c = 1; console.log(c); // 1 var e = function _e() {}; (function x() {}); } test(10);

这次我们讨论作用域链 Scope Chain 

定义

如果要简要说明,作用域链主要与内部函数有关

正如我们所知,ECMAScript允许创建内部函数,我们甚至可以从父函数返回这些内部函数

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

众所周知,每个上下文都有自己的变量对象:对于全局上下文,其变量对象就是全局对象本身,对于函数,其变量对象是活动对象

作用域链是内部上下文的所有变量对象的列表,该作用域链用于变量查找,在上面的例子中,“bar”上下文的作用域链包括AO(bar),AO(foo)和VO(global)

让我们从定义开始,进一步讨论更多的例子

作用域链与执行上下文相关联,是一条变量对象的链,用于在处理标识符时的变量查找

函数上下文的作用域链在函数调用时创建,由该函数的活动对象和内部[[Scope]]属性组成

用伪代码可以表示为:

activeExecutionContext = { VO: {...}, // or AO this: thisValue, Scope: [ // Scope chain // list of all variable objects // for identifiers lookup ] };

根据定义,Scope可以表示为:

Scope = AO + [[Scope]] 

我们可以将Scope和[[Scope]]表示为ECMAScript数组:

var Scope = [VO1, VO2, ..., VOn]; // scope chain

我们下面将讨论AO + [[Scope]]组合以及标识符解析过程,都与函数生命周期有关

函数生命周期

函数的生命周期分为创建阶段和激活(调用)阶段

函数创建

众所周知,函数声明在进入上下文阶段时被放入变量/活动对象(VO / AO)中,让我们看一下全局上下文中的变量和函数声明(其中变量对象是全局对象本身):

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

在函数激活时,我们看到了正确(预期)的结果 => 30

在这里,我们看到“y”变量在函数“foo”中定义(这意味着它在“foo”上下文的AO中),但变量“x”没有在“foo”的上下文中定义,因此不会被添加到“foo”的AO

乍一看,“foo”函数根本不存在“x”变量,正如我们将在下面看到的,“foo”上下文的活动对象只包含一个属性“y”:

fooContext.AO = { y: undefined // undefined – on entering the context, 20 – at activation };

函数“foo”如何访问“x”变量呢?函数应该可以访问更高层上下文的变量对象,实际上,确实如此,这个机制是通过函数的内部[[Scope]]属性来实现的

[[Scope]]是包含了所有父级变量对象的层级链,它位于当前函数上下文中,在函数创建时被保存到函数中 [[Scope]]是在创建函数时保存的,静态的(不变的),只有一次并且一直都存在,直到函数销毁

注意一点,[[Scope]]与Scope(作用域链)是不同的,前者是函数的属性,后者是上下文的属性 以上述例子为例,“foo”函数的[[Scope]]如下所示:

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

之后,函数调用时,会进入一个函数上下文,其中活动对象被创建,并且this值和Scope(作用域链)被确定

函数激活

正如定义中提到的那样,在进入上下文并且在创建AO / VO之后,上下文的Scope属性(作用域链,用于变量查找)定义为:

Scope = AO|VO + [[Scope]] 

这里要强调活动对象是Scope数组的第一个元素,即添加到作用域链的最前面:

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

这个特征对标识符解析过程非常重要

标识符解析是确定变量(或函数声明)属于作用域链中哪个变量对象的过程

这个算法返回的是一个Reference类型的值,其base属性是相应的变量对象(如果没有找到变量,则为null),其property name属性的名字是查找到的标识符的名称,细节可参考 this

标识符解析的过程包括与变量名称对应的属性查找,即从作用域链的最底层上下文一直到最上层上下文

因此,查找过程中上下文的局部变量比父上下文的变量具有更高的优先级,如果两个相同名字的变量存在于不同的上下文中时,处于底层上下文的变量会优先被找到

让我们看一个稍微复杂的例子:

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

上述代码,对应了如下的变量/活动对象,函数的[[Scope]]属性以及上下文的作用域链:

全局上下文的变量对象是:

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]] // i.e.: 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

作用域的特性

让我们考虑一些与作用域链和函数[[Scope]]属性相关的重要特性

闭包

ECMAScript中的闭包与函数的[[Scope]]属性直接相关,正如前面指出的那样,[[Scope]]在创建函数时保存并存在,直到函数对象被销毁。实际上,闭包恰好是函数代码和其[[Scope]]属性的组合,因此,[[Scope]]包含了函数创建所在的词法环境(父变量对象),上层上下文中的变量,可以在函数激活的时候,通过变量对象的词法链(函数创建时保存)查找到

例子:

var x = 10; function foo() { alert(x); } (function () { var x = 20; foo(); // 10, but not 20 })();

我们看到x变量在foo函数的[[Scope]中被找到,也就是说,变量的查找是在函数创建时定义的词法(闭包)链,而不是调用的动态链(否则x变量将被解析为20)

闭包的另一个经典例子:

function foo() { var x = 10; var y = 20; return function () { alert([x, y]); }; } var x = 30; var bar = foo(); // anonymous function is returned bar(); // [10, 20]

我们再次看到,对于标识符解析,使用函数创建时定义的词法作用域链,变量x被解析为10,而不是30 此外,这个例子清楚地表明函数的[[Scope]]属性,即使在函数上下文已经结束,也会继续存在

通过Function构造器创建的函数的[[Scope]]属性

在上面的例子中,我们看到函数创建时就获得[[Scope]]属性,并通过此属性访问所有父上下文的变量,但这有一个重要的例外,就是通过Function构造器创建的函数

var x = 10; function foo() { var y = 20; function barFD() { // FunctionDeclaration alert(x); alert(y); } var barFE = function () { // FunctionExpression alert(x); alert(y); }; var barFn = Function('alert(x); alert(y);'); barFD(); // 10, 20 barFE(); // 10, 20 barFn(); // 10, "y" is not defined } foo();

正如我们所看到的,对于通过Function构造器创建的barFn函数,变量y不可访问,但它并不意味着barFn函数没有内部的[[Scope]]属性(否则它将无法访问变量x)

问题是通过Function构造器创建的函数的[[Scope]]属性始终只包含全局对象

二维作用域链查找

在作用域链查找中的一个重点是变量对象的原型,因为ECMAScript的原型特性: 如果在对象中没有直接找到属性,则查找会在原型链中进行

  • 在作用域链的链接上
  • 在每个作用域链接上,深入原型链链接
    如果在Object.prototype中定义属性,我们可以观察到这种效果:
function foo() { alert(x); } Object.prototype.x = 10; foo(); // 10

活动对象没有原型,我们可以在下面的例子中看到:

function foo() { var x = 20; function bar() { alert(x); } bar(); } Object.prototype.x = 10; foo(); // 20

如果bar函数上下文的活动对象有一个原型,那么属性x应该在Object.prototype中找到,因为它不存在于AO中 但是在上面的第一个例子中,遍历标识符查找中的作用域链,我们到达全局对象,该对象从Object.prototype继承,因此x被解析为10

全局和eval上下文的作用域链

全局上下文的作用域链中只包含全局对象 “eval”代码的上下文和调用上下文(calling context)有相同的作用域链

globalContext.Scope = [ Global ]; evalContext.Scope === callingContext.Scope;


引用

ECMA-262-3 in detail. Chapter 4. Scope chain.


原文发布时间:2018-03-06

本文来源掘金如需转载请紧急联系作者

原文链接:https://yq.aliyun.com/articles/610212
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章