擒賊先擒王,簡單談一下JavaScript做用域鏈(Scope Chain)

alt

前言

咱們都知道一個執行上下文的數據(變量、函數聲明和函數的形參)做爲屬性存儲在變量對象中,同時咱們也應該知道變量對象在每次進入上下文時建立並填入初始值,值的更新出如今代碼執行階段。那麼我們專門討論與執行上下文直接相關的更多細節,此次咱們將說起一個議題——做用域鏈。html

英文原文:http://dmitrysoshnikov.com/ecmascript/chapter-4-scope-chain/
中文參考:http://www.denisdeng.com/?p=908
本文絕大部份內容來自上述地址,僅作少量修改,感謝做者
若有雷同,純屬抄襲,吼吼
複製代碼

定義

若是要簡要的描述並展現其重點,那麼做用域鏈大多處與內部函數相關。前端

咱們知道,ECMAScript容許建立內部函數,咱們甚至能從父函數中返回這些函數。算法

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

foo()() // 30
複製代碼

這樣,很明顯每一個上下文都擁有本身的變量對象:對於全局上下文,它是全局對象自身;對於函數,它是活動對象。數組

做用域鏈正是內部上下文全部變量對象(包括父變量對象)的列表,此鏈用來變量查詢。即在上面的的例子中,「bar」上下文的做用域包括AO(foo)、AO(foo)和VO(global)bash

可是,讓咱們仔細討論這個問題。閉包

讓咱們從定義開始,並進一步的討論示例。ecmascript

做用域鏈與一個執行上下文相關,變量對象的鏈用於在標識符解析中變量查找ide

函數上下文的做用域鏈在函數調用時建立的,包含活動對象和這個函數內部的[scope]屬性,下面咱們詳細討論一個函數[scope]屬性。函數

在上下文中示意以下:ui

activeExecutionContext = {
    VO: {...}, // or AO
    this: thisValue,
    Scope: [ // Scope chain
      // 全部變量對象的列表
      // for identifiers lookup
    ]
};
複製代碼

其scope定義以下:

Scope = 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 – at activation
};
複製代碼

函數「foo」如何訪問到變量「x」?理論上函數應該能訪問一個更高一層上下文的變量對象。實際上它正是這樣,這種機制經過函數內部的[[scope]]屬性來實現的,[[scope]]是全部父變量對象的層級鏈,處於當前函數上下文之上,在函數建立時存於其中。

注意這重要的一點 [[scope]]在函數建立時被存儲,永遠永遠,直至函數被銷燬,即:函數能夠永遠不調用,但[[scope]]屬性已經寫入,並儲存在函數對象中。

另一個須要考慮的是,與做用域鏈對比,[[scope]]是函數的一個屬性而不是上下文。考慮到上面的例子,函數「foo」的[[scope]]以下:

foo.[[Scope]] = [
  globalContext.VO // === Global
];
複製代碼

舉例來講,咱們用一般的ECMAScript 數組展示做用域和[[scope]]。 繼而,咱們知道在函數調用時進入上下文,這時候活動對象被建立,this和做用域(做用域鏈)被肯定。讓咱們詳細考慮這一時刻。

函數激活

正如在定義中說到的,進入上下文建立AO/VO以後,上下文的Scope屬性(變量查找的一個做用域鏈)做以下定義:

Scope = AO|VO + [[Scope]]
複製代碼

上面代碼的意思是:活動對象是做用域數組的第一個對象,即添加到做用域的前端。

Scope = [AO].concat([[Scope]]);
複製代碼

這個特色對於標示符解析的處理來講很重要

標示符解析是一個處理過程,用來肯定一個變量(或函數聲明)屬於哪一個變量對象。

這個算法的返回值中,咱們總有一個引用類型,它的base組件是相應的變量對象(或若未找到則爲null),屬性名組件是向上查找的標示符的名稱。標識符解析過程包含與變量名對應屬性的查找,即做用域中變量對象的連續查找,從最深的上下文開始,繞過做用域鏈直到最上層。這樣一來,在向上查找中,一個上下文中的局部變量較之於父做用域的變量擁有較高的優先級。萬一兩個變量有相同的名稱但來自不一樣的做用域,那麼第一個被發現的是在最深做用域中.

咱們用一個稍微複雜的例子描述上面講到的這些。

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,
  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
複製代碼

做用域特徵

閉包

在ECMAScript中,閉包與函數的[[scope]]直接相關,正如咱們提到的那樣,[[scope]]在函數建立時被存儲,與函數共存亡。實際上,閉包是函數代碼和其[[scope]]的結合。所以,做爲其對象之一,[[Scope]]包括在函數內建立的詞法做用域(父變量對象)。當函數進一步激活時,在變量對象的這個詞法鏈(靜態的存儲於建立時)中,來自較高做用域的變量將被搜尋。

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

咱們再次看到,在標識符解析過程當中,使用函數建立時定義的詞法做用域--變量解析爲10,而不是30。此外,這個例子也清晰的代表,一個函數(這個例子中爲從函數「foo」返回的匿名函數)的[[scope]]持續存在,即便是在函數建立的做用域已經完成以後。

經過構造函數建立的函數的[[scope]]

在上面的例子中,咱們看到,在函數建立時得到函數的[[scope]]屬性,經過該屬性訪問到全部父上下文的變量。可是,這個規則有一個重要的例外,它涉及到經過函數構造函數建立的函數。

var x = 10;
 
function foo() {
 
  var y = 20;
 
  function barFD() { // 函數聲明
    alert(x);
    alert(y);
  }
 
  var barFE = function () { // 函數表達式
    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 constructor)建立的函數「bar」,是不能訪問變量「y」的。但這並不意味着函數「barFn」沒有[[scope]]屬性(不然它不能訪問到變量「x」)。問題在於經過函構造函數建立的函數的[[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
複製代碼

全局和eval上下文中的做用域鏈

這裏不必定頗有趣,但必需要提示一下。全局上下文的做用域鏈僅包含全局對象。代碼eval的上下文與當前的調用上下文(calling context)擁有一樣的做用域鏈。

globalContext.Scope = [
  Global
];
 
evalContext.Scope === callingContext.Scope;
複製代碼

代碼執行時對做用域鏈的影響

在ECMAScript 中,在代碼執行階段有兩個聲明能修改做用域鏈。這就是with聲明和catch語句。它們添加到做用域鏈的最前端,對象須在這些聲明中出現的標識符中查找。若是發生其中的一個,做用域鏈簡要的做以下修改:

Scope = withObject|catchObject + AO|VO + [[Scope]]
複製代碼

在這個例子中添加對象,對象是它的參數(這樣,沒有前綴,這個對象的屬性變得能夠訪問)。

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

做用域鏈修改爲這樣:

Scope = foo + AO|VO + [[Scope]]
複製代碼

咱們再次看到,經過with語句,對象中標識符的解析添加到做用域鏈的最前端:

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
複製代碼

在進入上下文時發生了什麼?標識符「x」和「y」已被添加到變量對象中。此外,在代碼運行階段做以下修改:

  • x = 10, y = 10;
  • 對象{x:20}添加到做用域的前端;
  • 在with內部,遇到了var聲明,固然什麼也沒建立,由於在進入上下文時,全部變量已被解析添加;
  • 在第二步中,僅修改變量「x」,實際上對象中的「x」如今被解析,並添加到做用域鏈的最前端,「x」爲20,變爲30;
  • 一樣也有變量對象「y」的修改,被解析後其值也相應的由10變爲30;
  • 此外,在with聲明完成後,它的特定對象從做用域鏈中移除(已改變的變量「x」--30也從那個對象中移除),即做用域鏈的結構恢復到with獲得增強之前的狀態。
  • 在最後兩個alert中,當前變量對象的「x」保持同一,「y」的值如今等於30,在with聲明運行中已發生改變

一樣,catch語句的異常參數變得能夠訪問,它建立了只有一個屬性的新對象--異常參數名。圖示看起來像這樣:

try {
  ...
} catch (ex) {
  alert(ex);
}
複製代碼

做用域鏈修改成:

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

結論

在這個階段,咱們幾乎考慮了與執行上下文相關的全部經常使用概念,以及與它們相關的細節。按照計劃--函數對象的詳細分析:函數類型(函數聲明,函數表達式)和閉包。順便說一下,工做之餘隨手總結只爲和你們一塊兒分享實屬不易,若有雷同、純屬抄襲,吼吼歡迎你們一塊兒討論技術。若是以爲對您有幫助請給小編點下當心心。

轉自 深刻理解JavaScript系列 但願對你們有所幫助

相關文章
相關標籤/搜索