Javascript Closure

modun1986發表於2009-03-16

翻譯原文:http://blog.csdn.net/Ant_Yan/archive/2008/09/24/2972572.aspx

英文原文:http://jibbering.com/faq/faq_notes/closures.html

 

Closure介紹

Js閉包是一種封閉程式碼塊(一般來說是一個函式),它包含了自由變數和繫結這些自由變數的環境,這些變數不是在這個程式碼塊或者全域性作用域定義的,而是在定義程式碼塊的環境中定義。

 

閉包是Javascript中最強大的特性之一,但是在完全理解它之前這種強大的功能很難能發揮出來。建立閉包相對而言比較簡單,有時甚至是意外的建立了閉包,而意外閉包的建立往往存在潛伏的危害,特別是當它存在於瀏覽器的公共環境中。 為了避免意外的引入有危害的閉包、而又要受益於閉包的強大功能,就有必要了解它的機制。首先要了解的是作用域鏈(Scope Chain)Javascript如何定位變數的、而每個物件的property值又是如何解析的。

 

另一種對Js閉包的理解是:Javascript允許內部函式,即函式的宣告定義是完全被包含在另外一個函式的函式體之內。這個內部(Inner)函式有許可權訪問自身和它外部(Outer)函式所有的local變數、parameter(Js沒有塊級作用域)。當這個內部函式在包含它的Outer函式之外仍然可以訪問的時候,一個閉包就形成了。這個時候所有Outer函式的local變數、parameter值包括Inner函式的宣告也是處於可訪問狀態。這些變數和Inner函式宣告是保持上一次Outer函式呼叫返回時候的值。

 

物件的屬性名解析

ECMAScript(Javascript) 識別兩種型別的物件,Native物件和Host物件,Native物件也有另外一種名稱叫做Built-in(內建)物件。內建物件來源於Js語言本身,而Host物件是執行環境中產生的,比如document物件和DOM各節點物件等。

 

內建物件的屬性名命名很寬鬆,每個命名的屬性具有一個值,可能引用另外一個物件也可能是一個基本型別資料值,這裡函式也屬於物件,基本資料包括StringNumberNull or Undefined。這個Undefined或許有些奇怪因為具有Undefined值的屬性其實有定義的(至少該命名的屬性是存在),只不過具有的值是Undefined而已。當一個屬性被賦值時,如果取屬性的物件並沒有定義這個屬性,一個同名新的屬性就會被定義並完成賦值,如果已經定義就執行re-set操作。

 

讀取值的時候由於每個物件都有原型物件prototype屬性,而這個prototype本身也是一個物件,物件就可以繫結屬性值,當這個prototype物件上含有要讀取的同名屬性時,它的值就會被返回作為讀取結果。那麼既然prototype也是物件,物件又會具有它的prototype,這樣就會形成一個原型鏈。當原型鏈中某個物件擁有一個為nullprototype屬性時原型鏈就終結。預設情況下Object的建構函式具有一個null值的原型鏈。所以var ref= new Object() 將會建立一個物件,它具有的原型物件Object.prototype值為null

 

function MyObject1(param1){

         this.testNumber = param1;

}

function MyObject2(param2){

         This.testString = param2;

}

MyObject2.prototype = new MyObject1(8);

var ref = new MyObject2(“String value”);

 

MyObject2的這個例項ref擁有一個原型鏈。鏈上第一個物件是一個MyObject1的例項物件,這個物件建立出來然後賦值或者說掛載在MyObject2的原型物件上。而MyObject1有自己的原型鏈,鏈上僅有一個物件就是預設的Object.prototype,由於它已經為null原型鏈的解析終結。當從ref從讀取testString值時它直接從MyObject2中找到改屬性並返回;當讀取testNumber時需要遍歷到原型鏈中的MyObject1例項上才能找到匹配的屬性,當讀取toString屬性時就遍歷到了Object基類的原型物件上才找到匹配屬性。如果都無法匹配則返回undefined

 

讀取值的順序是依次讀取物件自身域、然後是原型物件域、然後原型域物件的原型域,遞迴下去直到為原型屬性為null,遇到第一個匹配的屬性值就返回結果。

 

執行上下文(Execution Context)

執行上下文(Execution context)是一個抽象的概念,用來定義Javascript指令碼執行的環境。所有Js程式碼將在上下文中執行,全域性Js將在一個叫做全域性上下文的環境中執行,所有函式呼叫(包括構造物件)將會產生一個函式上下文,由eval呼叫執行的Js也會關聯到一個特定的執行上下文中(雖然幾乎部怎麼使用)

 

當一個Js函式被呼叫時就進入了它對應的函式上下文,當在這個上下文中另外一個函式被呼叫或者本身函式被二次呼叫時,一個新的函式上下文又會產生。這個函式上下文一直持續存在到函式呼叫結束。執行環境將會退出到第一個函式上下文中,於是這裡就形成了一個執行上下文的棧式結構 (呼叫棧)

 

當一個函式上下文被建立的時候,一系列事情按順序發生。首先,在函式上下文中有一個叫做”Activation Object”的物件被建立出來,它屬於另外一種建立機制因為它具有某些命名屬性類似物件但又不具有prototype屬性並且無法被Js程式碼直接引用。下一步,在函式上下文中建立arguments物件,一個類似陣列的結構,可以按照整數下標順序取出裡面的元素。它具有lengthcallee屬性。之前的Activation Object有一個同名為arguments的屬性將作為arguments物件的引用。再下一步,這個函式上下文會被賦值或者說掛載到一個作用域。一個作用域包含了一連串物件。每一個函式物件也將包含內建作用域屬性包含著一連串的物件,即函式關聯的所有屬性值,函式作用域鏈最終被掛載到函式上下文上,而Activation Object就作為這條作用域鏈的第一個元素。

 

再下一步將是變數初始化的過程,這個時候Activation Object充當一個變數載體的角色,稱之為”Variable Object”,根據函式引數名命名相應的屬性,如果引數與arguments陣列元素正好數量對應則依次賦值,否則若arguments過長則多出來的值沒有屬性名,如果arguments元素不夠則有些引數會賦值undefined。內部函式會以它宣告的名字作為屬性名儲存到Variable Object上,最後一步初始化就是針對所有的local變數根據屬性名賦初始化值為undefined,所有的local變數只有在真正呼叫時才會根據實際情況被賦值。從Activation ObjectVariable Object其實是同一個物件的角度來理解,arguments也成了一個local變數。

 

終於函式上下文建立完畢,之後所有的變數訪問都必須使用this關鍵字,即使沒有顯式也會被訪問器自動加上this字首。如果屬性屬於Object即要麼有值要麼為undefinedthis引用這個物件例項,若為null則說明該物件不包含這個屬性this引用全域性物件。

 

全域性上下文比較特殊,它不存在arguments屬性所以也不需要定義Activation Object,它也不需要一個作用域物件,因為它的作用域裡永遠只有一個物件就是全域性物件自身。它也不必執行變數初始化的過程,對於全域性物件來說所有的inner函式其實就是最上層宣告的普通函式而已。不過它同樣會使用this關鍵字指向這個全域性物件,它僅僅簡單的被當作一個變數載體來使用,所以所有的頂層函式和全域性變數其實都是這個全域性物件的屬性之一。

 

作用域鏈(Scope Chain)

函式呼叫時作用域鏈是通過掛載/新增執行上下文中Variable Object作為頭節點來構造的,本身它也作為函式物件的一個屬性。由於Javascript中函式也是物件,它們在掃描函式宣告的變數初始化階段就已經被建立,建立時候呼叫了Functionconstructor,這裡的Function是指Js的一個內建物件。通過這個構造器建立的函式物件都會有一個屬性指向作用域鏈初始化包含唯一的全域性物件。

 

當構造唯一的全域性物件時,掃描所有的頂層函式並完成對全域性物件的作用域鏈屬性解析,所有頂層函式物件的作用域鏈也是在這個時候形成,並且都會帶有全域性物件的引用。

 

內部函式會造成函式物件的構造在另外一個函式上下文環境中,這就使得它的作用域鏈物件更加複雜。考慮一個例子況:當一個Outer函式被呼叫時,會建立一個新的函式上下文環境,這個上下文環境的作用鏈包括一個新的Activation Object和函式物件本身的作用域鏈(scope)屬性。在變數初始化階段一個內部函式物件被建立並帶有自身的作用域鏈屬性,這個內部函式物件的作用域鏈依次掛載了此函式的Activation Object和全域性物件。到目前為止,這一系列動作都是在原始碼和Js指令碼語言機制下自動完成的,執行上下文的作用域鏈屬性定義了所有被建立的函式物件,而函式物件的作用域鏈屬性定義了它們被呼叫時啟動新的上下文環境的作用域。ECMAScript提供了一個”with”語法來意圖修改作用域鏈。

 

with 語句接受一個表示式引數,如果表示式是一個物件,它將會被掛載到當前執行上下文環境的作用域鏈上來,並且位置位於Activation Object之前。接下來with執行一個塊語句,執行完畢會還原之前的作用域鏈。函式宣告無法在with語句塊中發揮作用,因為它僅僅建立了一個函式物件不存在執行環境,但函式表示式可以在with語句塊中被執行。

 

標識解析(Identifier Resolution)

標識的解析正好跟作用域鏈相反,ECMA規範把this當作一個關鍵字而不是標識是不太合理的,因為每次都是在執行上下文中都是依賴this來解析標識而不是通過作用域鏈。標識的解析從作用域鏈的第一個元素開始,先檢查第一個作用域中是否存在這個標識,因為作用域鏈上的都是一個個物件,所以這種檢查也會覆蓋物件的原型鏈。如果第一個作用域物件上無法找到標識同名的屬性名就檢查第二個,依次遍歷下去直到直到該屬性或者作用域鏈條終結。對已經找到解析的標識進行操作會跟操作物件的屬性遵循一樣的流程(/)

 

函式被呼叫時關聯上的執行上下文將會把Activation Object放在作用域鏈的第一個位置,標識解析也會首先檢查Activation Object物件的屬性,看是否能在函式引數、內部函式名、local變數中找到對應的名字。

Closures (閉包)

垃圾回收機制 (Garbage Collection)

ECMAScript使用自動垃圾回收機制,規範中並沒有定義垃圾回收的細節,而是把具體實現留給了各瀏覽器廠商,比如有一些實現賦予垃圾回收器非常低的優先順序(可能導致記憶體洩漏)。一般而言預設的原則是一個無法被引用了的物件,即所有對它的引用都變得不可訪問的時候,它就變成了一個可回收的物件並且在將來的某個時間會被銷燬,它佔用的一切資源也可以釋放。

 

垃圾回收典型的觸發時機是當一個執行上下文環境退出時,作用域鏈結構、Activation Object和所有在執行上下文環境中建立的物件,包括函式物件,都將變得不可訪問。所以,對垃圾收集器來說它們都可回收。

 

形成閉包

閉包是這樣形成的:在Outer函式呼叫時產生執行上下文環境,Outer函式中存在一個Inner函式,內部函式物件的引用作為返回值退出了Outer函式,被賦給了另外一個物件的某個屬性或者某全域性變數。例:

  1. function exampleClosureForm(arg1, arg2){
  2.     var localVar = 8;
  3.     function exampleReturned(innerArg){
  4.         return (arg1+arg2)/(innerArg+localVar);
  5.     }
  6.     return exampleReturned;
  7. }
  8. var globalVar = exampleClosuresForm(2,4);

現在對於exampleClosureForm的這次呼叫建立的執行上下文來說, 內部建立的函式物件無法被垃圾收集器回收,因為它仍然處於可訪問狀態,可以通過globalVar(5)來觸發它。然而現在有更復雜的事情發生了,就是globalVar引用的函式物件自身帶有一個作用域鏈屬性,包含著一個Activation Object元素,這個Activation Obje

相關文章