閉包函式

閉包函式

閉包是ECMAScript (JavaScript)最強大的特性之一,但用好閉包的前提是必須理解閉包。閉包的創建相對容易,人們甚至會在不經意間創建閉包,但這些無意創建的閉包卻存在潛在的危害,尤其是在比較常見的瀏覽器環境下。如果想要揚長避短地使用閉包這一特性,則必須了解它們的工作機制。而閉包工作機制的實現很大程度上有賴於標識符(或者說對象屬性)解析過程中作用域的角色。

基本介紹

  • 中文名:閉包函式
  • 外文名:ECMAScript (JavaScript)
  • 適用領域範圍:程式
  • 前提:好閉包的前提是必須理解閉包
函式簡介,對象屬性名解析,標識符解析、執行環境和作用域鏈,回到頂部,標識符解析,自動垃圾收集,構成閉包,套用範圍,

函式簡介

關於閉包,最簡單的描述就是 ECMAScript 允許使用內部函式--即函式定義和函式表達式位於另一個函式的函式體內。而且,這些內部函式可以訪問它們所在的外部函式中聲明的所有局部變數、參數和聲明的其他內部函式。當其中一個這樣的內部函式在包含它們的外部函式之外被調用時,就會形成閉包。也就是說,內部函式會在外部函式返回後被執行。而當這個內部函式執行時,它仍然必需訪問其外部函式的局部變數、參數以及其他內部函式。這些局部變數、參數和函式聲明(最初時)的值是外部函式返回時的值,但也會受到內部函式的影響。
遺憾的是,要適當地理解閉包就必須理解閉包背後運行的機制,以及許多相關的技術細節。雖然本文的前半部分並沒有涉及 ECMA 262 規範指定的某些算法,但仍然有許多無法迴避或簡化的內容。對於個別熟悉對象屬性名解析的人來說,可以跳過相關的內容,但是除非你對閉包也非常熟悉,否則最好是不要跳過下面幾節。

對象屬性名解析

ECMAScript 認可兩類對象:原生(Native)對象和宿主(Host)對象,其中宿主對象包含一個被稱為內置對象的原生對象的子類(ECMA 262 3rd Ed Section 4.3)。原生對象屬於語言,而宿主對象由環境提供,比如說可能是文檔對象、DOM 等類似的對象。
原生對象具有鬆散和動態的命名屬性(對於某些實現的內置對象子類別而言,動態性是受限的--但這不是太大的問題)。對象的命名屬性用於保存值,該值可以是指向另一個對象(Objects)的引用(在這個意義上說,函式也是對象),也可以是一些基本的數據類型,比如:String、Number、BooleanNullUndefined。其中比較特殊的是 Undefined 類型,因為可以給對象的屬性指定一個 Undefined 類型的值,而不會刪除對象的相應屬性。而且,該屬性只是保存著 undefined 值。
下面簡要介紹一下如何設定和讀取對象的屬性值,並最大程度地體現相應的內部細節。
值的賦予對象的命名屬性可以通過為該命名屬性賦值來創建,或重新賦值。即,對於:
var objectRef = new Object(); //創建一個普通的 javascript 對象。
可以通過下面語句來創建名為 “testNumber” 的屬性:
objectRef.testNumber = 5;/* - 或- */objectRef["testNumber"] = 5;
在賦值之前,對象中沒有“testNumber” 屬性,但在賦值後,則創建一個屬性。之後的任何賦值語句都不需要再創建這個屬性,而只會重新設定它的值:
objectRef.testNumber = 8;/* - 或- */objectRef["testNumber"] = 8;
稍後我們會介紹,Javascript 對象都有原型(prototypes)屬性,而這些原型本身也是對象,因而也可以帶有命名的屬性。但是,原型對象命名屬性的作用並不體現在賦值階段。同樣,在將值賦給其命名屬性時,如果對象沒有該屬性則會創建該命名屬性,否則會重設該屬性的值。 值的讀取當讀取對象的屬性值時,原型對象的作用便體現出來。如果對象的原型中包含屬性訪問器(property accessor)所使用的屬性名,那么該屬性的值就會返回: /* 為命名屬性賦值。如果在賦值前對象沒有相應的屬性,那么賦值後就會得到一個:
*/objectRef.testNumber = 8;/* 從屬性中讀取值 */var val = objectRef.testNumber;/*
現在, - val - 中保存著剛賦給對象命名屬性的值 8*/而且,由於所有對象都有原型,而原型本身也是對象,所以原型也可能有原型,這樣就構成了所謂的原型鏈。原型鏈終止於鏈中原型為 null 的對象。Object 構造函式的默認原型就有一個 null 原型,因此:
JavaScript教程JavaScript教程
var objectRef = new Object(); //創建一個普通的 JavaScript 對象。
創建了一個原型為 Object.prototype 的對象,而該原型自身則擁有一個值為 null 的原型。也就是說,objectRef 的原型鏈中只包含一個對象-- Object.prototype。但對於下面的代碼而言:
/* 創建 - MyObject1 - 類型對象的函式*/
function MyObject1(formalParameter){
/* 給創建的對象添加一個名為 - testNumber -
的屬性並將傳遞給構造函式的第一個參數指定為該屬性的值:*/
this.testNumber = formalParameter;
}
/* 創建 - MyObject2 - 類型對象的函式*/
function MyObject2(formalParameter){
/* 給創建的對象添加一個名為 - testString -
的屬性並將傳遞給構造函式的第一個參數指定為該屬性的值:*/
this.testString = formalParameter;
}
/* 接下來的操作用 MyObject1 類的實例替換了所有與 MyObject2
類的實例相關聯的原型。而且,為 MyObject1 構造函式傳遞了參數
- 8 - ,因而其 - testNumber - 屬性被賦予該值:*/
MyObject2.prototype = new MyObject1( 8 );
/* 最後,將一個字元串作為構造函式的第一個參數,
創建一個 - MyObject2 - 的實例,並將指向該對象的
引用賦給變數 - objectRef - :*/
var objectRef = new MyObject2( "String_Value" );
被變數 objectRef 所引用的 MyObject2 的實例擁有一個原型鏈。該鏈中的第一個對象是在創建後被指定給 MyObject2 構造函式的 prototype 屬性的 MyObject1 的一個實例。MyObject1 的實例也有一個原型,即與 Object.prototype 所引用的對象對應的默認的 Object 對象的原型。最後, Object.prototype 有一個值為 null 的原型,因此這條原型鏈到此結束。
當某個屬性訪問器嘗試讀取由 objectRef 所引用的對象的屬性值時,整個原型鏈都會被搜尋。在下面這種簡單的情況下:
var val = objectRef.testString;
因為 objectRef 所引用的 MyObject2 的實例有一個名為“testString”的屬性,因此被設定為“String_Value”的該屬性的值被賦給了變數 val。但是:
var val = objectRef.testNumber;
則不能從 MyObject2 實例自身中讀取到相應的命名屬性值,因為該實例沒有這個屬性。然而,變數 val 的值仍然被設定為 8,而不是未定義--這是因為在該實例中查找相應的命名屬性失敗後,解釋程式會繼續檢查其原型對象。而該實例的原型對象是 MyObject1 的實例,這個實例有一個名為“testNumber”的屬性並且值為 8,所以這個屬性訪問器最後會取得值 8。而且,雖然 MyObject1 和 MyObject2 都沒有定義 toString 方法,但是當屬性訪問器通過 objectRef 讀取 toString 屬性的值時:
var val = objectRef.toString;
變數 val 也會被賦予一個函式的引用。這個函式就是在 Object.prototype 的 toString 屬性中所保存的函式。之所以會返回這個函式,是因為發生了搜尋objectRef 原型鏈的過程。當在作為對象的 objectRef 中發現沒有“toString”屬性存在時,會搜尋其原型對象,而當原型對象中不存在該屬性時,則會繼續搜尋原型的原型。而原型鏈中最終的原型是 Object.prototype,這個對象確實有一個 toString 方法,因此該方法的引用被返回。
最後:
var val = objectRef.madeUpProperty;
返回 undefined,因為在搜尋原型鏈的過程中,直至 Object.prototype 的原型--null,都沒有找到任何對象有名為“madeUpPeoperty”的屬性,因此最終返回 undefined。
不論是在對象或對象的原型中,讀取命名屬性值的時候只返回首先找到的屬性值。而當為對象的命名屬性賦值時,如果對象自身不存在該屬性則創建相應的屬性。
這意味著,如果執行像 objectRef.testNumber = 3 這樣一條賦值語句,那么這個 MyObject2 的實例自身也會創建一個名為“testNumber”的屬性,而之後任何讀取該命名屬性的嘗試都將獲得相同的新值。這時候,屬性訪問器不會再進一步搜尋原型鏈,但 MyObject1 實例值為 8 的“testNumber”屬性並沒有被修改。給 objectRef 對象的賦值只是遮擋了其原型鏈中相應的屬性。
注意:ECMAScript 為 Object 類型定義了一個內部 [[prototype]] 屬性。這個屬性不能通過腳本直接訪問,但在屬性訪問器解析過程中,則需要用到這個內部 [[prototype]] 屬性所引用的對象鏈--即原型鏈。可以通過一個公共的 prototype 屬性,來對與內部的 [[prototype]] 屬性對應的原型對象進行賦值或定義。這兩者之間的關係在 ECMA 262(3rd edition)中有詳細描述,但超出了本文要討論的範疇。

標識符解析、執行環境和作用域鏈

執行環境
執行環境是 ECMAScript 規範(ECMA 262 第 3 版)用於定義 ECMAScript 實現必要行為的一個抽象的概念。對如何實現執行環境,規範沒有作規定。但由於執行環境中包含引用規範所定義結構的相關屬性,因此執行環境中應該保有(甚至實現)帶有屬性的對象--即使屬性不是公共屬性。
所有 JavaScript 代碼都是在一個執行環境中被執行的。全局代碼(作為內置的 JS 檔案執行的代碼,或者 HTML 頁面載入的代碼)是在我將稱之為“全局執行環境”的執行環境中執行的,而對函式的每次調用(有可能是作為構造函式)同樣有關聯的執行環境。通過
當調用一個 JavaScript 函式時,該函式就會進入相應的執行環境。如果又調用了另外一個函式(或者遞歸地調用同一個函式),則又會創建一個新的執行環境,並且在函式調用期間執行過程都處於該環境中。當調用的函式返回後,執行過程會返回原始執行環境。因而,運行中的 JavaScript 代碼就構成了一個執行環境棧。
在創建執行環境的過程中,會按照定義的先後順序完成一系列操作。首先,在一個函式的執行環境中,會創建一個“活動”對象。活動對象是規範中規定的另外一種機制。之所以稱之為對象,是因為它擁有可訪問的命名屬性,但是它又不像正常對象那樣具有原型(至少沒有預定義的原型),而且不能通過 JavaScript 代碼直接引用活動對象。
函式調用創建執行環境的下一步是創建一個arguments 對象,這是一個類似數組的對象,它以整數索引的數組成員一一對應地保存著調用函式時所傳遞的參數。這個對象也有 length 和 callee 屬性(這兩個屬性與我們討論的內容無關,詳見規範)。然後,會為活動對象創建一個名為“arguments”的屬性,該屬性引用前面創建的 arguments 對象。
接著,為執行環境分配作用域。作用域由對象列表(鏈)組成。每個函式對象都有一個內部的 [[scope]] 屬性(該屬性我們稍後會詳細介紹),這個屬性也由對象列表(鏈)組成。指定給一個函式調用執行環境的作用域,由該函式對象的 [[scope]] 屬性所引用的對象列表(鏈)組成,同時,活動對象被添加到該對象列表的頂部(鏈的前端)。
之後會發生由 ECMA 262 中所謂“可變”對象完成的“變數實例化”的過程。只不過此時使用活動對象作為可變對象(這裡很重要,請注意:它們是同一個對象)。此時會將函式的形式參數創建為可變對象命名屬性,如果調用函式時傳遞的參數與形式參數一致,則將相應參數的值賦給這些命名屬性(否則,會給命名屬性賦 undefined 值)。對於定義的內部函式,會以其聲明時所用名稱為可變對象創建同名屬性,而相應的內部函式則被創建為函式對象並指定給該屬性。變數實例化的最後一步是將在函式內部聲明的所有局部變數創建為可變對象的命名屬性。
根據聲明的局部變數創建的可變對象的屬性在變數實例化過程會被賦予 undefined 值。在執行函式體內的代碼、並計算相應的賦值表達式之前不會對局部變數執行真正的實例化。
事實上,擁有 arguments 屬性的活動對象和擁有與函式局部變數對應的命名屬性的可變對象是同一個對象。因此,可以將標識符 arguments 作為函式的局部變數來看待。

回到頂部

最後,在this可以被使用之前,還必須先對其賦值。如果賦的值是一個對象的引用,則 this.m 訪問的便是該對象上的 m。如果(內部)賦的值是 null,則this就指向全局對象。 (此段由 pangba 劉未鵬 翻譯)
(原文備考:Finally a value is assigned for use with the this keyword. If the value assigned refers to an object then property accessors prefixed with the this keyword reference properties of that object. If the value assigned (internally) is null then the this keyword will refer to the global object. )
創建全局執行環境的過程會稍有不同,因為它沒有參數,所以不需要通過定義的活動對象來引用這些參數。但全局執行環境也需要一個作用域,而它的作用域鏈實際上只由一個對象--全局對象--組成。全局執行環境也會有變數實例化的過程,它的內部函式就是涉及大部分 JavaScript 代碼的、常規的頂級函式聲明。而且,在變數實例化過程中全局對象就是可變對象,這就是為什麼全局性聲明
的函式是全局對象屬性的原因。全局性聲明的變數同樣如此。
全局執行環境也會使用 this 對象來引用全局對象。
作用域鏈與 [[scope]]
調用函式時創建的執行環境會包含一個作用域鏈,這個作用域鏈是通過將該執行環境的活動(可變)對象添加到保存於所調用函式對象的 [[scope]] 屬性中的作用域鏈前端而構成的。所以,理解函式對象內部的 [[scope]] 屬性的定義過程至關重要。
在 ECMAScript 中,函式也是對象。函式對象在變數實例化過程中會根據函式聲明來創建,或者是在計算函式表達式或調用 Function 構造函式時創建。
通過調用 Function 構造函式創建的函式對象,其內部的 [[scope]] 屬性引用的作用域鏈中始終只包含全局對象。
通過函式聲明或函式表達式創建的函式對象,其內部的 [[scope]] 屬性引用的則是創建它們的執行環境的作用域鏈。
在最簡單的情況下,比如聲明如下全局函式:-
function exampleFunction(formalParameter){
... // 函式體內的代碼
}
當為創建全局執行環境而進行變數實例化時,會根據上面的函式聲明創建相應的函式對象。因為全局執行環境的作用域鏈中只包含全局對象,所以它就給自己創建的、並以名為“exampleFunction”的屬性引用的這個函式對象的內部 [[scope]] 屬性,賦予了只包含全局對象的作用域鏈。
當在全局環境中計算函式表達式時,也會發生類似的指定作用域鏈的過程:-
var exampleFuncRef = function(){
... // 函式體代碼
}
在這種情況下,不同的是在全局執行環境的變數實例化過程中,會先為全局對象創建一個命名屬性。而在計算賦值語句之前,暫時不會創建函式對象,也不會將該函式對象的引用指定給全局對象的命名屬性。但是,最終還是會在全局執行環境中創建這個函式對象(當計算函式表達式時。譯者注),而為這個創建的函式對象的 [[scope]] 屬性指定的作用域鏈中仍然只包含全局對象。
內部的函式聲明或表達式會導致在包含它們的外部函式的執行環境中創建相應的函式對象,因此這些函式對象的作用域鏈會稍微複雜一些。在下面的代碼中,先定義了一個帶有內部函式聲明的外部函式,然後調用外部函式:
function exampleOuterFunction(formalParameter){
function exampleInnerFuncitonDec(){
... // 內部函式體代碼
}
... // 其餘的外部函式體代碼
}
exampleOuterFunction( 5 );
與外部函式聲明對應的函式對象會在全局執行環境的變數實例化過程中被創建。因此,外部函式對象的 [[scope]] 屬性中會包含一個只有全局對象的“單項目”作用域鏈。
當在全局執行環境中調用 exampleOuterFunction 函式時,會為該函式調用創建一個新的執行環境和一個活動(可變)對象。這個新執行環境的作用域就由新的活動對象後跟外部函式對象的 [[scope]] 屬性所引用的作用域鏈(只有全局對象)構成。在新執行環境的變數實例化過程中,會創建一個與內部函式聲明對應的函式對象,而同時會給這個函式對象的 [[scope]] 屬性指定創建該函式對象的執行環境(即新執行環境。譯者注)的作用域值--即一個包含活動對象後跟全局對象的作用域鏈。
到目前為止,所有過程都是自動、或者由原始碼的結構所控制的。但我們發現,執行環境的作用域鏈定義了執行環境所創建的函式對象的 [[scope]] 屬性,而函式對象的 [[scope]] 屬性則定義了它的執行環境的作用域(包括相應的活動對象)。不過,ECMAScript 也提供了用於修改作用域鏈 with 語句。
with 語句會計算一個表達式,如果該表達式是一個對象,那么就將這個對象添加到當前執行環境的作用域鏈中(在活動<可變>對象之前)。然後,執行 with 語句(它自身也可能是一個語句塊)中的其他語句。之後,又恢復到調用它之前的執行環境的作用域鏈中。
with 語句不會影響在變數實例化過程中根據函式聲明創建函式對象。但是,可以在一個 with 語句內部對函式表達式求值:-
/* 創建全局變數 - y - 它引用一個對象:- */
var y = {x:5}; // 帶有一個屬性 - x - 的對象直接量
function exampleFuncWith(){
var z;
/* 將全局對象 - y - 引用的對象添加到作用域鏈的前端:- */
with(y){
/* 對函式表達式求值以創建函式對象並將該函式對象的引用指定給局部變數 - z - :- */
z = function(){
... // 內部函式表達式中的代碼;
}
}
...
}
/* 執行 - exampleFuncWith - 函式:- */
exampleFuncWith();
在調用 exampleFuncWith 函式所創建的執行環境中包含一個由其活動對象後跟全局對象構成的作用域鏈。而在執行 with 語句時,又會把全局變數 y 引用的對象添加到這個作用域鏈的前端。在對其中的函式表達式求值的過程中,所創建函式對象的 [[scope]] 屬性與創建它的執行環境的作用域保持一致--即,該屬性會引用一個由對象 y 後跟調用外部函式時所創建執行環境的活動對象,後跟全局對象的作用域鏈。
當與 with 語句相關的語句塊執行結束時,執行環境的作用域得以恢復(y 會被移除),但是已經創建的函式對象(z。譯者注)的 [[scope]] 屬性所引用的作用域鏈中位於最前面的仍然是對象 y。

標識符解析

標識符是沿作用域鏈逆向解析的。ECMA 262 將 this 歸類為關鍵字而不是標識符,並非不合理。因為解析 this 值時始終要根據使用它的執行環境來判斷,而與作用域鏈無關。
標識符解析從作用域鏈中的第一個對象開始。檢查該對象中是否包含與標識符對應的屬性名。因為作用域鏈是一條對象鏈,所以這個檢查過程也會包含相應對象的原型鏈(如果有)。如果沒有在作用域鏈的第一個對象中發現相應的值,解析過程會繼續搜尋下一個對象。這樣依次類推直至找到作用域鏈中包含以標識符為屬性名的對象為止,也有可能在作用域鏈的所有對象中都沒有發現該標識符。
基於對象使用屬性訪問器時,也會發生與上面相同的標識符解析過程。當屬性訪問器中有相應的屬性可以替換某個對象時,這個屬性就成為表示該對象的標識符,該對象在作用域鏈中的位置進而被確定。全局對象始終都位於作用域鏈的尾端。
因為與函式調用相關的執行環境將會把活動(可變)對象添加到作用域鏈的前端,所以在函式體內使用的標識符會首先檢查自己是否與形式參數、內部函式聲明的名稱或局部變數一致。這些都可以由活動(可變)對象的命名屬性來確定。

自動垃圾收集

ECMAScript 要求使用自動垃圾收集機制。但規範中並沒有詳細說明相關的細節,而是留給了實現來決定。但據了解,相當一部分實現對它們的垃圾收集操作只賦予了很低的優先權。但是,大致的思想都是相同的,即如果對象不再“可引用(由於不存在對它的引用,使執行代碼無法再訪問到它)”時,該對象就成為垃圾收集的目標。因而,在將來的某個時刻會將這個對象銷毀並將它所占用的一切資源釋放,以便作業系統重新利用。
正常情況下,當退出一個執行環境時就會滿足類似的條件。此時,作用域鏈結構中的活動(可變)對象以及在該執行環境中創建的任何對象--包括函式對象,都不再“可引用”,因此將成為垃圾收集的目標。

構成閉包

閉包是通過在對一個函式調用的執行環境中返回一個函式對象構成的。比如,在對函式調用的過程中,將一個對內部函式對象的引用指定給另一個對象的屬性。或者,直接將這樣一個(內部)函式對象的引用指定給一個全局變數、或者一個全局性對象的屬性,或者一個作為參數以引用方式傳遞給外部函式的對象。例如:-
function exampleClosureForm(arg1, arg2){
var localVar = 8;
function exampleReturned(innerArg){
return ((arg1 + arg2)/(innerArg + localVar));
}
/* 返回一個定義為 exampleReturned 的內部函式的引用 -:- */
return exampleReturned;
}
var globalVar = exampleClosureForm(2, 4);
這種情況下,在調用外部函式 exampleClosureForm 的執行環境中所創建的函式對象就不會被當做垃圾收集,因為該函式對象被一個全局變數所引用,而且仍然是可以訪問的,甚至可以通過 globalVar(n) 來執行。
的確,情況比正常的時候要複雜一些。因為現在這個被變數 globalVar 引用的內部函式對象的 [[scope]] 屬性所引用的作用域鏈中,包含著屬於創建該內部函式對象的執行環境的活動對象(和全局對象)。由於在執行被 globalVar 引用的函式對象時,每次都要把該函式對象的 [[scope]] 屬性所引用的整個作用域鏈添加到創建的(內部函式的)執行環境的作用域中(即此時的作用域中包括:內部執行環境的活動對象、外部執行環境的活動對象、全局對象。譯者注), 所以這個(外部執行環境的)活動對象不會被當作垃圾收集。
閉包因此而構成。此時,內部函式對象擁有自由的變數,而位於該函式作用域鏈中的活動(可變)對象則成為與變數綁定的環境。
由於活動(可變)對象受限於內部函式對象(現在被 globalVar 變數引用)的 [[scope]] 屬性中作用域鏈的引用,所以活動對象連同它的變數聲明--即屬性的值,都會被保留。而在對內部函式調用的執行環境中進行作用域解析時,將會把與活動(可變)對象的命名屬性一致的標識符作為該對象的屬性來解析。活動對象的這些屬性值即使是在創建它的執行環境退出後,仍然可以被讀取和設定。
在上面的例子中,當外部函式返回(退出它的執行環境)時,其活動(可變)對象的變數聲明中記錄了形式參數、內部函式定義以及局部變數的值。arg1 屬性的值為 2,而 arg2 屬性的值為 4,localVar 的值是 8,還有一個 exampleReturned 屬性,它引用由外部函式返回的內部函式對象。(為方便起見,我們將在後面的討論中,稱這個活動<可變>對象為 “ActOuter1″)。
如果再次調用 exampleClosureForm 函式,如:-
var secondGlobalVar = exampleClosureForm(12, 3);
- 則會創建一個新的執行環境和一個新的活動對象。而且,會返回一個新的函式對象,該函式對象的 [[scope]] 屬性引用的作用域鏈與前一次不同,因為這一次的作用域鏈中包含著第二個執行環境的活動對象,而這個活動對象的屬性 arg1 值為 12 而屬性 arg2 值為 3。(為方便起見,我們將在後面的討論中,稱這個活動<可變>對象為 “ActOuter2″)。
通過第二次執行 exampleClosureForm 函式,第二個、也是截然不同的閉包誕生了。
通過執行 exampleClosureForm 創建的兩個函式對象分別被指定給了全局變數 globalVar 和 secondGlobalVar,並返回了表達式 ((arg1 + arg2)/(innerArg + localVar))。該表達式對其中的四個標識符套用了不同的操作符。如何確定這些標識符的值是體現閉包價值的關鍵所在。
我們來看一看,在執行由 globalVar 引用的函式對象--如 globalVar(2)--時的情形。此時,會創建一個新的執行環境和相應的活動對象(我們將稱之為“ActInner1”),並把該活動對象添加到執行的函式對象的 [[scope]] 屬性所引用的作用域鏈的前端。ActInner1 會帶有一個屬性 innerArg,根據傳遞的形式參數,其值被指定為 2。這個新執行環境的作用域鏈變成: ActInner1->ActOuter1->全局對象.
為了返回表達式 ((arg1 + arg2)/(innerArg + localVar)) 的值,要沿著作用域鏈進行標識符解析。表達式中標識符的值將通過依次查找作用域鏈中每個對象(與標識符名稱一致)的屬性來確定。
作用域鏈中的第一個對象是 ActInner1,它有一個名為 innerArg 的屬性,值是 2。所有其他三個標識符在 ActOuter1 中都有對應的屬性:arg1 是 2,arg2 是 4 而 localVar 是 8。最後,函式調用返回 ((2 + 2)/(2 + 8))。
現在再來看一看由 secondGlobalVar 引用的同一個函式對象的執行情況,比如 secondGlobalVar(5)。我們把這次創建的新執行環境的活動對象稱為 “ActInner2”,相應的作用域鏈就變成了:ActInner2->ActOuter2->全局對象。ActInner2 返回 innerArg 的值 5,而 ActOuter2 分別返回 arg1、arg2 和 localVar 的值 12、3 和 8。函式調用返回的值就是 ((12 + 3)/(5 + 8))。
如果再執行一次 secondGlobalVar,則又會有一個新活動對象被添加到作用域鏈的前端,但 ActOuter2 仍然是鏈中的第二個對象,而他的命名屬性會再次用於完成標識符 arg1、arg2 和 localVar 的解析。
這就是 ECMAScript 的內部函式獲取、維持和訪問創建他們的執行環境的形式參數、聲明的內部函式以及局部變數的過程。這個過程說明了構成閉包以後,內部的函式對象在其存續過程中,如何維持對這些值的引用、如何對這些值進行讀取的機制。即,創建內部函式對象的執行環境的活動(可變)對象,會保留在該函式對象的 [[scope]] 屬性所引用的作用域鏈中。直到所有對這個內部函式的引用被釋放,這個函式對象才會成為垃圾收集的目標(連同它的作用域鏈中任何不再需要的對象)。
內部函式自身也可能有內部函式。在通過函式執行返回內部函式構成閉包以後,相應的閉包自身也可能會返回內部函式從而構成它們自己的閉包。每次作用域鏈嵌套,都會增加由創建內部函式對象的執行環境引發的新活動對象。ECMAScript 規範要求作用域鏈是臨時性的,但對作用域鏈的長度卻沒有加以限制。在具體實現中,可能會存在實際的限制,但還沒有發現有具體限制數量的報告。目前來看,嵌套的內部函式所擁有的潛能,仍然超出了使用它們的人的想像能力。

套用範圍

對這個問題的回答可能會令你驚訝--閉包什麼都可以做。據我所知,閉包使得 ECMAScript 能夠模仿任何事物,因此局限性在於設計和實現要模仿事物的能力。只是從字面上看可能會覺得這么說很深奧,下面我們就來看一些更有實際意義的例子。
例 1:為函式引用設定延時
閉包的一個常見用法是在執行函式之前為要執行的函式提供參數。例如:將函式作為 setTimout 函式的第一個參數,這在 Web 瀏覽器的環境下是非常常見的一種套用。
setTimeout 用於有計畫地執行一個函式(或者一串 JavaScript 代碼,不是在本例中),要執行的函式是其第一個參數,其第二個參數是以毫秒表示的執行間隔。也就是說,當在一段代碼中使用 setTimeout 時,要將一個函式的引用作為它的第一個參數,而將以毫秒表示的時間值作為第二個參數。但是,傳遞函式引用的同時無法為計畫執行的函式提供參數。
然而,可以在代碼中調用另外一個函式,由它返回一個對內部函式的引用,再把這個對內部函式對象的引用傳遞給 setTimeout 函式。執行這個內部函式時要使用的參數在調用返回它的外部函式時傳遞。這樣,setTimeout 在執行這個內部函式時,不用傳遞參數,但該內部函式仍然能夠訪問在調用返回它的外部函式時傳遞的參數:
function callLater(paramA, paramB, paramC){
/* 返回一個由函式表達式創建的匿名內部函式的引用:- */
return (function(){
/* 這個內部函式將通過 - setTimeout - 執行,
而且當它執行時它會讀取並按照傳遞給
外部函式的參數行事:
*/
paramA[paramB] = paramC;
});
}
...
/* 調用這個函式將返回一個在其執行環境中創建的內部函式對象的引用。
傳遞的參數最終將作為外部函式的參數被內部函式使用。
返回的對內部函式的引用被賦給一個全局變數:-
*/
var functRef = callLater(elStyle, "display", "none");
/* 調用 setTimeout 函式,將賦給變數 - functRef -
的內部函式的引用作為傳遞的第一個參數:- */
hideMenu=setTimeout(functRef, 500);
例 2: 通過對象實例方法關聯函式
回到頂部
許多時候我們需要將一個函式對象暫時掛到一個引用上留待後面執行,因為不等到執行的時候是很難知道其具體參數的,而先前將它賦給那個引用的時候更是壓根不知道的。 (此段由 pangba 劉未鵬 翻譯)
(luyy朋友的翻譯_2008-7-7更新)很多時候需要將一個函式引用進行賦值,以便在將來某個時候執行該函式,在執行這些函式時給函式提供參數將會是有用處的,但這些參數在執行時不容易獲得,他們只有在上面賦值給時才能確定。
(原文備考:There are many other circumstances when a reference to a function object is assigned so that it would be executed at some future time where it is useful to provide parameters for the execution of that function that would not be easily available at the time of execution but cannot be known until the moment of assignment.)
一個相關的例子是,用 JavaScript 對象來封裝與特定 DOM 元素的互動。這個 JavaScript 對象具有 doOnClick、doMouseOver 和 doMouseOut 方法,並且當用戶在該特定的 DOM 元素中觸發了相應的事件時要執行這些方法。不過,可能會創建與不同的 DOM 元素關聯的任意數量的 JavaScript 對象,而且每個對象實例並不知道實例化它們的代碼將會如何操縱它們(即註冊事件處理函式與定義相應的事件處理函式分離。譯者注)。這些對象實例並不知道如何在全局環境中引用它們自身,因為它們不知道將會指定哪個全局變數(如果有)引用它們的實例。
因而問題可以歸結為執行一個與特定的 JavaScript 對象關聯的事件處理函式,並且要知道調用該對象的哪個方法。
下面這個例子使用了一個基於閉包構建的一般化的函式(此句多謝未鵬指點),該函式會將對象實例與 DOM 元素事件關聯起來,安排執行事件處理程式時調用對象實例的指定方法,給象的指定方法傳遞的參數是事件對象和與元素關聯的引用,該函式返回執行相應方法後的返回值。
/* 一個關聯對象實例和事件處理器的函式。
它返回的內部函式被用作事件處理器。對象實例以 - obj - 參數表示,
而在該對象實例中調用的方法名則以 - methodName - (字元串)參數表示。
*/
function associateObjWithEvent(obj, methodName){
/* 下面這個返回的內部函式將作為一個 DOM 元素的事件處理器*/
return (function(e){
/* 在支持標準 DOM 規範的瀏覽器中,事件對象會被解析為參數 - e - ,
若沒有正常解析,則使用 IE 的事件對象來規範化事件對象。
*/
e = e||window.event;
/* 事件處理器通過保存在字元串 - methodName - 中的方法名調用了對象
- obj - 的一個方法。並傳遞已經規範化的事件對象和觸發事件處理器的元素
的引用 - this - (之所以 this 有效是因為這個內部函式是作為該元素的方法執行的)
*/
return obj[methodName](e, this);
});
}
/* 這個構造函式用於創建將自身與 DOM 元素關聯的對象,
DOM 元素的 ID 作為構造函式的字元串參數。
所創建的對象會在相應的元素觸發 _disibledevent= function(){
... // 方法體。
};
Internet Explorer 的記憶體泄漏問題
Internet Explorer Web 瀏覽器(在 IE 4 到 IE 6 中核實)的垃圾收集系統中存在一個問題,即如果 ECMAScript 和某些宿主對象構成了 "循環引用",那么這些對象將不會被當作垃圾收集。此時所謂的宿主對象指的是任何 DOM 節點(包括 document 對象及其後代元素)和 ActiveX 對象。如果在一個循環引用中包含了一或多個這樣的對象,那么這些對象直到瀏覽器關閉都不會被釋放,而它們所占用的記憶體同樣在瀏覽器關閉之前都不會交回系統重用。
當兩個或多個對象以首尾相連的方式相互引用時,就構成了循環引用。比如對象 1 的一個屬性引用了對象 2 ,對象 2 的一個屬性引用了對象 3,而對象 3 的一個屬性又引用了對象 1。對於純粹的 ECMAScript 對象而言,只要沒有其他對象引用對象 1、2、3,也就是說它們只是相互之間的引用,那么仍然會被垃圾收集系統識別並處理。但是,在 Internet Explorer 中,如果循環引用中的任何對象是 DOM 節點或者 ActiveX 對象,垃圾收集系統則不會發現它們之間的循環關係與系統中的其他對象是隔離的並釋放它們。最終它們將被保留在記憶體中,直到瀏覽器關閉。
閉包非常容易構成循環引用。如果一個構成閉包的函式對象被指定給,比如一個 DOM 節點的事件處理器,而對該節點的引用又被指定給函式對象作用域中的一個活動(或可變)對象,那么就存在一個循環引用。DOM_Node.onevent ->function_object.[[scope]] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。形成這樣一個循環引用是輕而易舉的,而且稍微瀏覽一下包含類似循環引用代碼的網站(通常會出現在網站的每個頁面中),就會消耗大量(甚至全部)系統記憶體。
多加注意可以避免形成循環引用,而在無法避免時,也可以使用補償的方法,比如使用 IE 的 onunload 事件來來清空(null)事件處理函式的引用。時刻意識到這個問題並理解閉包的工作機制是在 IE 中避免此類問題的關鍵。

相關詞條

熱門詞條

聯絡我們