重学JavaScript深刻理解系列(四)

JavaScript深刻理解——做用域链(Scope Chain)

概要

在第二章变量对象的时候,已经介绍过执行上下文的数据是以变量对象的属性的形式进行存储的。算法

还介绍了,每次进入执行上下文的时候,就会建立变量对象,而且赋予其属性初始值,随后在执行代码阶段会对属性值进行更新。数组

本文要与执行上下文密切相关的另一个重要的概念——做用域链(Scope Chain)。bash

定义

若要简单扼要对做用域链作个解释,那就是:做用域链和内部函数息息相关。

众所周知,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)。ecmascript

下面就来详细介绍下做用域链。ide

先从定义开始,随后再结合例子详细介绍:函数

做用域链是一条变量对象的链,它和执行上下文有关,用于在处理标识符时候进行变量查询。
复制代码

函数上下文的做用域链在函数调用的时候建立出来,它包含了活跃对象和该函数的内部[[Scope]]属性。关于[[Scope]]会在后面做详细介绍。post

大体表示以下:ui

activeExecutionContext = {
    VO: {...}, // 或者 AO
    this: thisValue,
    Scope: [ // 所用域链
      // 全部变量对象的列表
      // 用于标识符查询
    ]
};
复制代码

上述代码中的Scope定义为以下所示:

Scope = AO + [[Scope]]
复制代码

针对咱们的例子来讲,能够将Scope和[[Scope]]用普通的ECMAScript数组来表示:

var Scope = [VO1, VO2, ..., VOn]; // 做用域链
复制代码

除此以外,还能够用分层对象链的数据结构来表示,链中每个连接都有对父做用域(上层变量对象)的引用。这种表示方式和第二章中讨论的某些实现中__parent__的概念相对应:

var VO1 = {__parent__: null, ... other data}; -->
var VO2 = {__parent__: VO1, ... other data}; -->
// etc.
复制代码

然而,使用数组来表示做用域链会更方便,所以,咱们这里就采用数组的表示方式。 除此以外,不论在实现层是否采用包含__parent__特性的分层对象链的数据结构,标准自身对其作了抽象的定义“做用域链是一个对象列表”。 数组就是实现列表这一律念最好的选择。

下面将要介绍的 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中。乍一眼看过去,变量“x”压根就不在“foo”中存在; 然而,正如咱们下面要看到的——仅仅只是“乍一眼看过去“而已。咱们看到“foo”上下文的活跃对象中只包含一个属性——“y”:

fooContext.AO = {
  y: undefined // undefined – 在进入上下文时, 20 – 在激活阶段
};
复制代码

那么,“foo”函数究竟是如何访问到变量“x”的呢?一个顺其天然的想法是:函数应当有访问更高层上下文变量对象的权限。 而事实也恰是如此,就是经过函数的内部属性[[Scope]]来实现这一机制的。

[[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]]);
复制代码

此特性对处理标识符很是重要。

处理标识符其实就是一个肯定变量(或者函数声明)属于做用域链中哪一个变量对象的过程。
复制代码

此算法返回的老是一个引用类型的值,其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: 
};
复制代码

在“foo”函数建立的时候,其[[Scope]]属性以下所示:

foo.[[Scope]] = [
  globalContext.VO
];
复制代码

在“foo”函数激活的时候(进入上下文时),“foo”函数上下文的活跃对象以下所示:

fooContext.AO = {
  y: 20,
  bar: 
};
复制代码

同时,“foo”函数上下文的做用域链以下所示:

fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
 
fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];
复制代码

在内部“bar”函数建立的时候,其[[Scope]]属性以下所示:

bar.[[Scope]] = [
  fooContext.AO,
  globalContext.VO
];
复制代码

在“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。 而且,上述例子清楚的展现了函数(上述例子中指的是函数“foo”返回的匿名函数)的[[Scope]]属性,即便在建立该函数的上下文结束的时候依然存在。

更多关于ECMAScript对闭包的实现细节会在第六章-闭包中作介绍。

经过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();
复制代码

上述例子中,函数“barFn”就是经过Function构造器来建立的,这个时候变量“y”就没法访问到了。 但这并不意味着函数“barFn”就没有内部的[[Scope]]属性了(不然它连变量“x”都没法访问到了)。 问题就在于当函数经过Function构造器来建立的时候,其[[Scope]]属性永远都只包含全局对象。 哪怕在上层上下文中(非全局上下文)建立一个闭包都是无济于事的。

二维做用域链查询

在做用域链查询的时候还有很重要的一点:变量对象的原型(若是有的话)也是须要考虑的——由于原型是ECMAScript天生的特性:若是属性在对象中没有找到,那么会继续经过原型链进行查询。 比方说以下这些二维链:(1)在做用域链的连接上,(2)在每一个做用域连接上——深刻到原型链的连接上。若是在原型链(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。

一样的状况,在某些版本的SpiderMonkey中,经过命名函数表达式(简称:NFE)也会发生,其中的存储了可选的函数表达式的名字的特殊对象也继承自Object.prototype, 一样的,在某些版本的Blackberry中,也是如此,其活跃对象是继承自Object.prototype的。不过,关于这块详细的特性将会在第五章-函数中做介绍。

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

尽管这部份内容没多大意思,但仍是值得一提的。全局上下文的做用域链中只包含全局对象。“eval”代码类型的上下文和调用上下文(calling context)有相同的做用域链。
globalContext.Scope = [
  Global
];
 
evalContext.Scope === callingContext.Scope;
复制代码

执行代码阶段对做用域的影响

ECMAScript中,在运行时,执行代码阶段有两种语句能够修改做用域链——with语句和catch从句。在标识符查询阶段,这二者都会被添加到做用域链的最前面。 好比,当有with或者catch的时候,做用域链就会被修改以下形式:
Scope = withObject|catchObject + AO|VO + [[Scope]]
复制代码

以下例子中,with语句添加了foo对象,使得它的属性能够不须要前缀直接访问。

var foo = {x: 10, y: 20};
 
with (foo) {
  alert(x); // 10
  alert(y); // 20
}
复制代码

对应的做用域链修改成以下所示:

Scope = foo + AO|VO + [[Scope]]
复制代码

接着来看下面这个例子:

var x = 10, y = 10;
 
with ({x: 20}) {
 
  var x = 30, y = 30;
 
  alert(x); // 30
  alert(y); // 30
}
 
alert(x); // 10
alert(y); // 30
复制代码

发生了什么?怎么最外层的“y”变成了30? 在进入上下文的时候,“x”和“y”标识符已经添加到了变量对象。以后,到了执行代码阶段,发生了以下的改动:

  • x=10, y=10
  • 对象{x: 20}添加到了做用域链的最前面
  • 在with中遇到了var语句,固然了,这个时候什么也不会发生。由于早在进入上下文阶段全部的变量都已经解析过了而且添加到了对应的变量对象上了。
  • 这里修改了“x”的值,本来“x”是在第二步的时候添加的对象{x: 20}(该对象被添加到了做用域链的最前面)中的“x”,如今变成了30。
  • 一样的,“y”的值也修改了,由本来的10变成了30
  • 以后,在with语句结束以后,其特殊对象从做用域链中移除(修改过的“x”——30,也随之移除),做用域链又恢复到了with语句前的状态。
  • 正如在最后两个alert中看到的,“x”的值恢复到了原先的10,而“y”的值由于在with语句的时候被修改过了,所以变为了30。

一样的,catch从句(能够访问参数异常)会建立一个只包含一个属性(异常参数名)的新对象。以下所示:

try {
  ...
} catch (ex) {
  alert(ex);
}
复制代码

做用域链修改成以下所示:

var catchObject = {
  ex: 
};
 
Scope = catchObject + AO|VO + [[Scope]]
复制代码

在catch从句结束后,做用域链一样也会恢复到原先的状态。

总结

本文,介绍了几乎全部与执行上下文相关的概念以及相应的细节。后面的章节中,会给你们介绍函数对象的细节:函数的类型(FunctionDeclaration,FunctionExpression)和闭包。 顺便提下,本文中介绍过,闭包是和[[Scope]]有直接的关系,可是关于闭包的细节会在后续章节中做介绍。

原文地址
译文地址

重学系列传送门

重学JavaScript深刻理解系列(一)
重学JavaScript深刻理解系列(二)
重学JavaScript深刻理解系列(三)
重学JavaScript深刻理解系列(五)
重学JavaScript深刻理解系列(六)

相关文章
相关标签/搜索