重學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深刻理解系列(六)

相關文章
相關標籤/搜索