深入理解JavaScript内部原理(4): 作用域链(Scope Chain)

本文是翻译http://dmitrysoshnikov.com/ecmascript/chapter-4-scope-chain/

概要

As we already know from the second chapter concerning the variable object, the data of an execution context (variables, function declarations, and formal parameters of functions) are stored as properties of the variables object.

Also, we know that the variable object is created and filled with initial values every time on entering the context, and that its updating occurs at code execution phase.

This chapter is devoted one more detail directly related with execution contexts; this time, we will mention a topic of a scope chain.

从第二章节关于变量对象描述中,我们已经知道,执行上下文的数据(变量,函数定义,函数的形参)是以属性的形式存储在变量对象中。

同样,我们也知道在每次进入上下文的时候, 变量对象被创建和初始化,并在代码执行的时候更改。

定义

If to describe briefly and showing the main point, a scope chain is mostly related with inner functions.

如果要简短的描述作用域链的重点,作用域链大多与内部函数有关。

As we know, ECMAScript allows creation of inner functions and we can even return these functions from parent functions.

我们知道,ECMAScript允许创建内部函数并且可以在父函数内返回这些函数。

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

Thus, is known that 
every context has its own variables object: for the global context it is 
global objectitself, for functions it is the 
activation object.
因此,很明显每一个上下文都有它自己的变量对象:如果是全局上下文就是全局对象自己,如果是函数,那么就是激活对象。

And the scope chain is exactly this list of all (parent) variable objects for the inner contexts. This chain is used for variables lookup. I.e. in the example above, scope chain of “bar” context includes AO(bar), AO(foo) and VO(global).

作用域链就是所有内部函数上下文变量对象(包括父级变量)的一个列表。作用域链用作查询,例如:在上面的例子中,

“bar”作用域链包括 AO(bar),AO(foo)和VO(global)

But, let’s examine this topic in detail.

Let’s begin with the definition and further will discuss deeper on examples.

Scope chain is related with an execution context a chain of variable objects which is used for variables lookup at identifier resolution.

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

The scope chain of a function context is created at function call and consists of the activation objectand the internal [[Scope]] property of this function. We will discuss the [[Scope]] property of a function in detail below.

函数上下文作用域链是在函数调用的时候创建的,包含活动对象和函数内部(scope)属性。下面我们将详细的讨论下函数内部的(scope)属性

Schematically in the context:

 

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

where Scope by definition is:

Scope = AO + [[Scope]]

For our examples we can represent Scope, and [[Scope]] as normal ECMAScript arrays:

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

The alternative structure view can be represented as a hierarchical object chain with the reference to the parent scope (to the parent variable object) on every link of the chain. For this view corresponds __parent__ concept of some implementations which we discussed in the second chapter devoted variable object:

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

But to represent a scope chain using an array is more convenient, so we will use this approach. Besides, the specification statements abstractly itself (see 10.1.4) that “a scope chain is a list of objects”, regardless that on the implementation level can be used the approach with the hierarchical chain involving the __parent__ feature. And the array abstract representation is a good candidate for the list concept.

The combination AO + [[Scope]] and also process of identifier resolution, which we will discuss below, are related with the life cycle of functions.

这种联合和标示符的解析过程,我们将在下面讨论,这与函数的生命周期相关。

函数的生命周期

Function life cycle is divided into a stage of creation and a stage of activation (call). Let’s consider them in detail.

函数的生命周期分为创建和激活阶段(调用的时候)。下面我们详细的讨论它。

函数的创建

As is known, function declarations are put into variable/activation object (VO/AO) on entering the context stage. Let’s see on the example a variable and a function declaration in the global context (where variable object is the global object itself, we remember, yes?):

我们知道,在进入上下文的时候,函数声明被存储到变量对象/活动对象中(VO/AO).下面我们以一个例子来看一下在全局上下文中变量和函数的声明(还记得变量对象是全局对象自己?是的)

 

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

At function activation, we see correct (and expected) result – 30. However, there is one very important feature.

在函数激活的时候,我们看到了正确(预期)的结果:30,但是,这里有一个很重要的特点。

Before this moment we spoke only about variable object of the current context. Here we see that “y” variable is defined in function “foo” (which means it is in the AO of “foo” context), but variable “x” is not defined in context of “foo” and accordingly is not added into the AO of “foo”. At first glance “x” variable does not exist at all for function “foo”; but as we will see below — only “at first glance”. We see that the activation object of “foo” context contains only one property — property “y”:

此前,我们仅仅谈到有关当前上下文的变量对象。这里,我们看到变量“y”在函数“foo”中定义(意思就是它在foo上下文的AO中),但是变量“x”并未在“foo”上下文中定义,相应地,它也不会添加到“foo”的AO中。乍一看,变量“x”相对于函数“foo”根本就不存在;但正如我们在下面看到的——也仅仅是“一瞥”,我们发现,“foo”上下文的活动对象中仅包含一个属性--“y”。

 

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

How does function “foo” have access to “x” variable? It is logical to assume that function should have access to the variable object of a higher context. In effect, it is exactly so and, physically this mechanism is implemented via the internal [[Scope]] property of a function.

[[Scope]] is a hierarchical chain of all parent variable objects, which are above the current function context; the chain is saved to the function at its creation.

函数“foo”如何访问到变量“x”?理论上函数应该能访问一个更高一层上下文的变量对象。实际上它正是这样,这种机制是通过函数内部的[[scope]]属性来实现的。

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

Notice the important point — [[Scope]] is saved at function creation — statically (invariably), once and forever — until function destruction. I.e. function can be never called, but [[Scope]] property isalready written and stored in function object.

Another moment which should be considered is that [[Scope]] in contrast with Scope (Scope chain) is the property of a function instead of a context. Considering the above example, [[Scope]] of the “foo” function is the following:

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

另外一个需要考虑的是--与作用域链对比,[[scope]]是函数的一个属性而不是上下文。考虑到上面的例子,函数“foo”的[[scope]]如下:

举例来说,我们用通常的ECMAScript 数组展现作用域和[[scope]]。

 

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

And further, by a function call as we know, there is an entering a function context where theactivation object is created and this value and Scope (Scope chain) are determined. Let us consider this moment in detail.

继续,我们知道在函数调用时进入上下文,这时候活动对象被创建,this和作用域(作用域链)被确定。让我们详细考虑这一时刻。

函数激活

As it has been said in definition, on entering the context and after creation of AO/VO, Scopeproperty of the context (which is a scope chain for variables lookup) is defined as follows:

正如在定义中说到的,进入上下文创建AO/VO之后,上下文的Scope属性(变量查找的一个作用域链)作如下定义:

Scope = AO|VO + [[Scope]]

High light here is that the activation object is the first element of the Scope array, i.e. added to thefront of scope chain:

上面代码的意思是:活动对象是作用域数组的第一个对象,即添加到作用域的前端。

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

This feature is very important for the process of identifier resolution.

Identifier resolution is a process of determination to which variable object in scope chain the variable (or the function declaration) belongs.

On return from this algorithm we have always a value of type Reference, which base component is the corresponding variable object (or null if variable is not found), and a property name component is the name of the looked up (resolved) identifier. In detail Reference type is discussed in theChapter 3. This.

Process of identifier resolution includes lookup of the property corresponding to the name of the variable, i.e. there is a consecutive examination of variable objects in the scope chain, starting from the deepest context and up to the top of the scope chain.

Thus, local variables of a context at lookup have higher priority than variables from parent contexts, and in case of two variables with the same name but from different contexts, the first is found the variable of deeper context.

这个特点对于标示符解析的处理来说是很重要的。

标示符解析是一个处理过程,用来确定一个变量(或函数声明)属于哪个变量对象。

这个算法的返回值中,我们总有一个引用类型,它的base组件是相应的变量对象(或若未找到则为null),属性名组件是向上查找的标示符的名称。引用类型的详细信息在第3章.this中已讨论。

标识符解析过程包含与变量名对应属性的查找,即作用域中变量对象的连续查找,从最深的上下文开始,绕过作用域链直到最上层。

这样一来,在向上查找中,一个上下文中的局部变量较之于父作用域的变量拥有较高的优先级。万一两个变量有相同的名称但来自不同的作用域,那么第一个被发现的是在最深作用域中。

Let’s a little complicate an example described above and add additional inner level:

看一个例子:

 

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

For which we have the following variable/activation objects[[Scope]] properties of functions andscope chains of contexts:

对此,我们有如下的变量/活动对象,函数的的[[scope]]属性以及上下文的作用域链:

Variable object of the global context is: 全局变量的上下文是

 

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

At “foo” creation, the [[Scope]] property of “foo” is: 当foo被创建时,foo的属性 scope 是:

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

At “foo” activation (on entering the context), the activation object of “foo” context is: 

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

 

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

And the scope chain of “foo” context is:

“foo”上下文的作用域链为:

 

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

At creation of inner “bar” function its [[Scope]] is:

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

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

At “bar” activation, the activation object of “bar” context is:

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

barContext.AO = {    z: 30 };

And the scope chain of “bar” context is:

因此“bar”作用域链是

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

Identifier resolution for “x”, “y” and “z” names:

对“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

作用域特性

Let’s consider some important features related with Scope chain and [[Scope]] property of functions.

让我们看看与作用域链和函数[[scope]]属性相关的一些重要特征。

闭包

Closures in ECMAScript are directly related with the [[Scope]] property of functions. As it has been noted, [[Scope]] is saved at function creation and exists until the function object is destroyed. Actually, a closure is exactly a combination of a function code and its [[Scope]] property. Thus, [[Scope]] contains that lexical environment (the parent variable object) in which function is created. Variables from higher contexts at the further function activation will be searched in this lexical (statically saved at creation) chain of variable objects.

在ECMAScript中,闭包与函数的[[scope]]直接相关,正如我们提到的那样,[[scope]]在函数创建时被存储,与函数共存亡。实际上,闭包是函数代码和其[[scope]]的结合。因此,作为其对象之一,[[Scope]]包括在函数内创建的词法作用域(父变量对象)。当函数进一步激活时,在变量对象的这个词法链(静态的存储于创建时)中,来自较高作用域的变量将被搜寻。

Examples:

 

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

We see that “x” variable is found in the [[Scope]] of “foo” function, i.e. for variables lookup the lexical (closured) chain defined at the moment of function creation, but not the dynamic chain of thecall (at which value of “x” variable would be resolved to 20) is used.

我们在”foo“函数的scope属性中发现了“x”变量。例如:在函数创建的时候,这个变量作用域链就已经定义了。而不是使用动态调用的时候。

Another (classical) example of closure:

 

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]

  

Again we see that for the identifier resolution the lexical scope chain defined at function creation is used — the variable “x” is resolved to 10, but not to 30. Moreover, this example clearly shows that [[Scope]] of a function (in this case of the anonymous function returned from function “foo”) continues to exist even after the context in which a function is created is already finished.

In more details about the theory of closures and their implementation in ECMAScript read in theChapter 6. Closures.

我们再次看到,在标识符解析过程中,使用函数创建时定义的词法作用域--变量解析为10,而不是30。此外,这个例子也清晰的表明,一个函数(这个例子中为从函数“foo”返回的匿名函数)的[[scope]]持续存在,即使是在函数创建的作用域已经完成之后。

关于ECMAScript中闭包的理论和其执行机制的更多细节,阅读6章闭包。

通过构造函数创建的函数scope属性

In the examples above we see that function at creation gets the [[Scope]] property and via this property it accesses variables of all parent contexts. However, in this rule there is one important exception, and it concerns functions created via the Function constructor.

在上面的例子中,我们看到,在函数创建时获得函数的[[scope]]属性,通过该属性访问到所有父上下文的变量。但是,这个规则有一个重要的例外,它涉及到通过函数构造函数创建的函数。

 

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();

As we see, for “barFn” function which is created via the Function constructor the variable “y” is not accessible. But it does not mean that function “barFn” has no internal [[Scope]] property (else it would not have access to the variable “x”). And the matter is that [[Scope]] property of functions created via the Function constructor contains always only the global object. Consider it since, for example, to create closure of upper contexts, except global, via such function is not possible.

通过这个例子我们可以看出,通过函数构造函数(Function constructor)创建的函数“bar”,是不能访问变量“y”的。但这并不意味着函数“barFn”没有[[scope]]属性(否则它不能访问到变量“x”)。原因在于通过函构造函数创建的函数的[[scope]]属性总是唯一的全局对象。考虑到这一点,如通过这种函数创建除全局之外的最上层的上下文闭包是不可能的。

二维作用域链查找

Also, an important point at lookup in scope chain is that prototypes (if they are) of variable objects can be also considered — because of prototypical nature of ECMAScript: if property is not found directly in the object, its lookup proceeds in the prototype chain. I.e. some kind of 2D-lookup of the chain: (1) on scope chain links, (2) and on every of scope chain link — deep into on prototype chain links. We can observe this effect if define property in Object.prototype:

在作用域链中查找最重要的一点是变量对象的属性(如果有的话)须考虑其中--源于ECMAScript 的原型特性。如果一个属性在对象中没有直接找到,查询将在原型链中继续。即常说的二维链查找。(1)作用域链环节;(2)每个作用域链--深入到原型链环节。如果在Object.prototype 中定义了属性,我们能看到这种效果。

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

Activation objects do not have prototypes what we can see in the following example:

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

 

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

If activation object of “bar” function context would have a prototype, then property “x” should be resolved in Object.prototype because it is not resolved directly in AO. But in the first example above, traversing the scope chain in identifier resolution, we reach the global object which (in some implementation but not in all) is inherited from Object.prototype and, accordingly, “x” is resolved to 10.

The similar situation can be observed in some versions of SpiderMokey with named function expressions (abbreviated form is NFE), where special object which stores the optional name of function-expression is inherited from Object.prototype, and also in some versions of Blackberryimplementation where activation objects are inherited from Object.prototype. But more detailed this features are discussed in Chapter 5. Functions.

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

Here is not so much interesting, but it is necessary to note. The scope chain of the global context contains only global object. The context with code type “eval” has the same scope chain as a calling context.

这里不一定很有趣,但必须要提示一下。全局上下文的作用域链仅包含全局对象。代码eval的上下文与当前的调用上下文(calling context)拥有同样的作用域链。

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

代码执行时对作用域链的影响

In ECMAScript there are two statements which can modify scope chain at runtime code execution phase. These are with statement and catch clause. Both of them add to the front of scope chain the object required for lookup identifiers appearing within these statements. I.e., if one of these case takes place, scope chain is schematically modified as follows:

在ECMAScript 中,在代码执行阶段有两个声明能修改作用域链。这就是with声明和catch语句。它们添加到作用域链的最前端,对象须在这些声明中出现的标识符中查找。如果发生其中的一个,作用域链简要的作如下修改:

Scope = withObject|catchObject + AO|VO + [[Scope]]

The statement with in this case adds the object which is its parameter (and thus properties of this object become accessible without prefix):

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

Scope chain modification:

Scope = foo + AO|VO + [[Scope]]

Let us show once again that the identifier is resolved in the object added by the with statement to the front of scope chain:

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

What happened here? On entering the context phase, “x” and “y” identifiers have been added into the variable object. Further, already at runtime code executions stage, following modifications have been made:

  • x = 10, y = 10;
  • the object {x: 20} is added to the front of scope chain;
  • the met var statement inside with, of course, created nothing, because all variables have been parsed and added on entering the context stage;
  • there is only modification of “x” value, and exactly that “x” which is resolved now in the object added to the front of scope chain at second step; value of this “x” was 20, and became 30;
  • also there is modification of “y” which is resolved in variable object above; accordingly, was 10, became 30;
  • further, after with statement is finished, its special objects is removed from the scope chain (and the changed value “x” – 30 is removed also with that object), i.e. scope chain structure is restored to the previous state which was before with statement augmentation;
  • as we see in last two alerts: the value of “x” in current variable object remains the same and the value of “y” is equal now to 30 and has been changed at with statement work.

在进入上下文时发生了什么?标识符“x”和“y”已被添加到变量对象中。此外,在代码运行阶段作如下修改:

  1. x = 10, y = 10;
  2. 对象{x:20}添加到作用域的前端;
  3. 在with内部,遇到了var声明,当然什么也没创建,因为在进入上下文时,所有变量已被解析添加;
  4. 在第二步中,仅修改变量“x”,实际上对象中的“x”现在被解析,并添加到作用域链的最前端,“x”为20,变为30;
  5. 同样也有变量对象“y”的修改,被解析后其值也相应的由10变为30;
  6. 此外,在with声明完成后,它的特定对象从作用域链中移除(已改变的变量“x”--30也从那个对象中移除),即作用域链的结构恢复到with得到加强以前的状态。
  7. 在最后两个alert中,当前变量对象的“x”保持同一,“y”的值现在等于30,在with声明运行中已发生改变。

Also, a catch clause in order to have access to the parameter-exception creates an intermediate scope object with the only property — exception parameter name, and places this object in front of the scope chain. Schematically it looks so:

try {    ... } catch (ex) {    alert(ex); }

Scope chain modification:

var catchObject = {    ex: <exception object> };   Scope = catchObject + AO|VO + [[Scope]]

After the work of catch clause is finished, scope chain is also restored to the previous state.

结论

At this stage, we have considerate almost all general concepts concerning execution contexts and related with them details. Further, according to plan, — detailed analysis of function objects: types of functions (FunctionDeclaration, FunctionExpression) and closures. By the way, closures are directly related with the [[Scope]] property discussed in this article, but about it is in appropriate chapter. I will be glad to answer your questions in comments.

在这个阶段,我们几乎考虑了与执行上下文相关的所有常用概念,以及与它们相关的细节。按照计划--函数对象的详细分析:函数类型(函数声明,函数表达式)和闭包。顺便说一下,在这篇文章中,闭包直接与[[scope]]属性相关,但是,关于它将在合适的篇章中讨论。我很乐意在评论中回答你的问题。

附加阅读

 

    原文作者:yupeng
    原文地址: https://www.cnblogs.com/yupeng/archive/2012/04/09/2438806.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞