什麼是JS的閉包

今天看javascript造成IE內存泄漏的文章,順便看了下閉包,以前只是對閉包粗略瞭解。今天看的這個介紹的特別詳細。特收錄到blog裏!
Closure
所謂“閉包”,指的是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。
閉包是 ECMAScript (JavaScript)最強大的特性之一,但用好閉包的前提是必須理解閉包。閉包的創建相對容易,人們甚至會在不經意間創建閉包,但這些無意創建的閉包卻存在潛在的危害,尤其是在比較常見的瀏覽器環境下。如果想要揚長避短地使用閉包這一特性,則必須瞭解它們的工作機制。而閉包工作機制的實現很大程度上有賴於標識符(或者說對象屬性)解析過程中作用域的角色。
關於閉包,最簡單的描述就是 ECMAScript 允許使用內部函數--即函數定義和函數表達式位於另一個函數的函數體內。而且,這些內部函數可以訪問它們所在的外部函數中聲明的所有局部變量、參數和聲明的其他內部函數。當其中一個這樣的內部函數在包含它們的外部函數之外被調用時,就會形成閉包。也就是說,內部函數會在外部函數返回後被執行。而當這個內部函數執行時,它仍然必需訪問其外部函數的局部變量、參數以及其他內部函數。這些局部變量、參數和函數聲明(最初時)的值是外部函數返回時的值,但也會受到內部函數的影響。
遺憾的是,要適當地理解閉包就必須理解閉包背後運行的機制,以及許多相關的技術細節。雖然本文的前半部分並沒有涉及 ECMA 262 規範指定的某些算法,但仍然有許多無法迴避或簡化的內容。對於個別熟悉對象屬性名解析的人來說,可以跳過相關的內容,但是除非你對閉包也非常熟悉,否則最好是不要跳下面幾節。
對象屬性名解析
ECMAScript 認可兩類對象:原生(Native)對象和宿主(Host)對象,其中宿主對象包含一個被稱爲內置對象的原生對象的子類(ECMA 262 3rd Ed Section 4.3)。原生對象屬於語言,而宿主對象由環境提供,比如說可能是文檔對象、DOM 等類似的對象。
原生對象具有鬆散和動態的命名屬性(對於某些實現的內置對象子類別而言,動態性是受限的--但這不是太大的問題)。對象的命名屬性用於保存值,該值可以是指向另一個對象(Objects)的引用(在這個意義上說,函數也是對象),也可以是一些基本的數據類型,比如:String、Number、 Boolean、Null 或 Undefined。其中比較特殊的是 Undefined 類型,因爲可以給對象的屬性指定一個 Undefined 類型的值,而不會刪除對象的相應屬性。而且,該屬性只是保存着 undefined 值。 
下面簡要介紹一下如何設置和讀取對象的屬性值,並最大程度地體現相應的內部細節。
值的賦予
對象的命名屬性可以通過爲該命名屬性賦值來創建,或重新賦值。即,對於:
var objectRef = new Object(); //創建一個普通的 JavaScript 對象。
可以通過下面語句來創建名爲 “testNumber” 的屬性:
objectRef.testNumber = 5;
/* - 或- */
objectRef[”testNumber”] = 5;
在賦值之前,對象中沒有“testNumber” 屬性,但在賦值後,則創建一個屬性。之後的任何賦值語句都不需要再創建這個屬性,而只會重新設置它的值:
objectRef.testNumber = 8;
/* - or:- */
objectRef[”testNumber”] = 8;
稍後我們會介紹,Javascript 對象都有原型(prototypes)屬性,而這些原型本身也是對象,因而也可以帶有命名的屬性。但是,原型對象命名屬性的作用並不體現在賦值階段。同樣,在將值賦給其命名屬性時,如果對象沒有該屬性則會創建該命名屬性,否則會重設該屬性的值。
值的讀取
當讀取對象的屬性值時,原型對象的作用便體現出來。如果對象的原型中包含屬性訪問器(property accessor)所使用的屬性名,那麼該屬性的值就會返回:
/* 爲命名屬性賦值。如果在賦值前對象沒有相應的屬性,那麼賦值後就會得到一個:*/
objectRef.testNumber = 8;
/* 從屬性中讀取值 */
var val = objectRef.testNumber;
/* 現在, - val - 中保存着剛賦給對象命名屬性的值 8*/
而且,由於所有對象都有原型,而原型本身也是對象,所以原型也可能有原型,這樣就構成了所謂的原型鏈。原型鏈終止於鏈中原型爲 null 的對象。Object 構造函數的默認原型就有一個 null 原型,因此:
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 頁面加載的代碼)是在我稱之爲“全局執行環境”的執行環境中執行的,而對函數的每次調用(
有可能是作爲構造函數)同樣有關聯的執行環境。通過 eval 函數執行的代碼也有截然不同的執行環境,但因爲 JavaScript 程序員在正常情況下一般不會使用 eval,所以這裏不作討論。有關執行環境的詳細說明請參閱 ECMA 262(3rd edition)第 10.2 節。
當調用一個 JavaScript 函數時,該函數就會進入相應的執行環境。如果又調用了另外一個函數(或者遞歸地調用同一個函數),則又會創建一個新的執行環境,並且在函數調用期間執行過程都處於該環境中。當調用的函數返回後,執行過程會返回原始執行環境。因而,運行中的 JavaScript 代碼就構成了一個執行環境棧。
在創建執行環境的過程中,會按照定義的先後順序完成一系列操作。首先,在一個函數的執行環境中,會創建一個“活動”對象。活動對象是規範中規定的另外一種機制。之所以稱之爲對象,是因爲它擁有可訪問的命名屬性,但是它又不像正常對象那樣具有原型(至少沒有預定義的原型),而且不能通過 JavaScript 代碼直接引用活動對象。
爲函數調用創建執行環境的下一步是創建一個 arguments 對象,這是一個類似數組的對象,它以整數索引的數組成員一一對應地保存着調用函數時所傳遞的參數。這個對象也有 length 和 callee 屬性(這兩個屬性與我們討論的內容無關,詳見規範)。然後,會爲活動對象創建一個名爲“arguments”的屬性,該屬性引用前面創建的 arguments對象。
接着,爲執行環境分配作用域。作用域由對象列表(鏈)組成。每個函數對象都有一個內部的 `scope` 屬性(該屬性我們稍後會詳細介紹),這個屬性也由對象列表(鏈)組成。指定給一個函數調用執行環境的作用域,由該函數對象的 `scope` 屬性所引用的對象列表(鏈)組成,同時,活動對象被添加到該對象列表的頂部(鏈的前端)。
之後會發生由 ECMA 262 中所謂“可變”對象完成的“變量實例化”的過程。只不過此時使用活動對象作爲可變對象(這裏很重要,請注意:它們是同一個對象)。此時會將函數的形式參數創建爲可變對象的命名屬性,如果調用函數時傳遞的參數與形式參數一致,則將相應參數的值賦給這些命名屬性(否則,會給命名屬性賦 undefined 值)。對於定義的內部函數,會以其聲明時所用名稱爲可變對象創建同名屬性,而相應的內部函數則被創建爲函數對象並指定給該屬性。變量實例化的最後一步是將在函數內部聲明的所有局部變量創建爲可變對象的命名屬性。
根據聲明的局部變量創建的可變對象的屬性在變量實例化過程中會被賦予 undefined 值。在執行函數體內的代碼、並計算相應的賦值表達式之前不會對局部變量執行真正的實例化。
事實上,擁有 arguments 屬性的活動對象和擁有與函數局部變量對應的命名屬性的可變對象是同一個對象。因此,可以將標識符 arguments 作爲函數的局部變量來看待。
最後,要爲使用 this 關鍵字而賦值。如果所賦的值引用一個對象,那麼前綴以 this 關鍵字的屬性訪問器就是引用該對象的屬性。如果所賦(內部)值是 null,那麼 this 關鍵字則引用全局對象。
創建全局執行環境的過程會稍有不同,因爲它沒有參數,所以不需要通過定義的活動對象來引用這些參數。但全局執行環境也需要一個作用域,而它的作用域鏈實際上只由一個對象--全局對象--組成。全局執行環境也會有變量實例化的過程,它的內部函數就是涉及大部分 JavaScript 代碼的、常規的頂級函數聲明。而且,在變量實例化過程中全局對象就是可變對象,這就是爲什麼全局性聲明的函數是全局對象屬性的原因。全局性聲明的變量同樣如此。
全局執行環境也會使用 this 對象來引用全局對象。
作用域鏈與 `scope`
調用函數時創建的執行環境會包含一個作用域鏈,這個作用域鏈是通過將該執行環境的活動(可變)對象添加到保存於所調用函數對象的 `scope` 屬性中的作用域鏈前端而構成的。所以,理解函數對象內部的 `scope` 屬性的定義過程至關重要。
在 ECMAScript 中,函數也是對象。函數對象在變量實例化過程中會根據函數聲明來創建,或者是在計算函數表達式或調用 Function 構造函數時創建。
通過調用 Function 構造函數創建的函數對象,其內部的 `scope` 屬性引用的作用域鏈中始終只包含全局對象。
通過函數聲明或函數表達式創建的函數對象,其內部的 `scope` 屬性引用的則是創建它們的執行環境的作用域鏈。
在最簡單的情況下,比如聲明如下全局函數:-
function exampleFunction(formalParameter){
… // 函數體內的代碼
}
- 當爲創建全局執行環境而進行變量實例化時,會根據上面的函數聲明創建相應的函數對象。因爲全局執行環境的作用域鏈中只包含全局對象,所以它就給自己創建的、並以名爲“exampleFunction”的屬性引用的這個函數對象的內部 `scope` 屬性,賦予了只包含全局對象的作用域鏈。
當在全局環境中計算函數表達式時,也會發生類似的指定作用域鏈的過程:-
var exampleFuncRef = function(){
… // 函數體代碼
}
在這種情況下,不同的是在全局執行環境的變量實例化過程中,會先爲全局對象創建一個命名屬性。而在計算賦值語句之前,暫時不會創建函數對象,也不會將該函數對象的引用指定給全局對象的命名屬性。但是,最終還是會在全局執行環境中創建這個函數對象(當計算函數表達式時。譯者注),而爲這個創建的函數對象的 `scope` 屬性指定的作用域鏈中仍然只包含全局對象。內部的函數聲明或表達式會導致在包含它們的外部函數的執行環境中創建相應的函數對象,因此這些函數對象的作用域鏈會稍微複雜一些。在下面的代碼中,先定義了一個帶有內部函數聲明的外部函數,然後調用外部函數:
 /* 創建全局變量 - 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。
mmentJS">
例 3:包裝相關的功能
閉包可以用於創建額外的作用域,通過該作用域可以將相關的和具有依賴性的代碼組織起來,以便將意外交互的風險降到最低。假設有一個用於構建字符串的函數,爲了避免重複性的連接操作(和創建衆多的中間字符串),我們的願望是使用一個數組按順序來存儲字符串的各個部分,然後再使用 Array.prototype.join 方法(以空字符串作爲其參數)輸出結果。這個數組將作爲輸出的緩衝器,但是將數組作爲函數的局部變量又會導致在每次調用函數時都重新創建一個新數組,這在每次調用函數時只重新指定數組中的可變內容的情況下並不是必要的。
一種解決方案是將這個數組聲明爲全局變量,這樣就可以重用這個數組,而不必每次都建立新數組。但這個方案的結果是,除了引用函數的全局變量會使用這個緩衝數組外,還會多出一個全局屬性引用數組自身。如此不僅使代碼變得不容易管理,而且,如果要在其他地方使用這個數組時,開發者必須要再次定義函數和數組。這樣一來,也使得代碼不容易與其他代碼整合,因爲此時不僅要保證所使用的函數名在全局命名空間中是唯一的,而且還要保證函數所依賴的數組在全局命名空間中也必須是唯一的。
而通過閉包可以使作爲緩衝器的數組與依賴它的函數關聯起來(優雅地打包),同時也能夠維持在全局命名空間外指定的緩衝數組的屬性名,免除了名稱衝突和意外交互的危險。
其中的關鍵技巧在於通過執行一個單行(in-line)函數表達式創建一個額外的執行環境,而將該函數表達式返回的內部函數作爲在外部代碼中使用的函數。此時,緩衝數組被定義爲函數表達式的一個局部變量。這個函數表達式只需執行一次,而數組也只需創建一次,就可以供依賴它的函數重複使用。
下面的代碼定義了一個函數,這個函數用於返回一個 HTMLHTML 字符串,其中大部分內容都是常量,但這些常量字符序列中需要穿插一些可變的信息,而可變的信息由調用函數時傳遞的參數提供。
通過執行單行函數表達式返回一個內部函數,並將返回的函數賦給一個全局變量,因此這個函數也可以稱爲全局函數。而緩衝數組被定義爲外部函數表達式的一個局部變量。它不會暴露在全局命名空間中,而且無論什麼時候調用依賴它的函數都不需要重新創建這個數組。
/* 聲明一個全局變量 - getImgInPositionedDivHtml -並將一次調用一個外部函數表達式返回的內部函數賦給它。         這個內部函數會返回一個用於表示絕對定位的 DIV 元素   包圍着一個 IMG 元素 的 HTMLHTML 字符串,這樣一來,   所有可變的屬性值都由調用該函數時的參數提供:*/var getImgInPositionedDivHtml = (function(){    /* 外部函數表達式的局部變量 - buffAr - 保存着緩衝數組。     這個數組只會被創建一次,生成的數組實例對內部函數而言永遠是可用的     因此,可供每次調用這個內部函數時使用。          其中的空字符串用作數據佔位符,相應的數據    將由內部函數插入到這個數組中:    */    var buffAr = [        ‘<div id=”‘,        ”,   //index 1, DIV ID 屬性        ‘” style=”position:absolute;top:’,        ”,   //index 3, DIV 頂部位置        ‘px;left:’,        ”,   //index 5, DIV 左端位置        ‘px;width:’,        ”,   //index 7, DIV 寬度        ‘px;height:’,        ”,   //index 9, DIV 高度        ‘px;overflow:hidden;”><img src=”‘,        ”,   //index 11, IMG URL        ‘” width=”‘,        ”,   //index 13, IMG 寬度        ‘” height=”‘,        ”,   //index 15, IMG 高度        ‘” alt=”‘,        ”,   //index 17, IMG alt 文本內容        ‘”></div>’    ];    /* 返回作爲對函數表達式求值後結果的內部函數對象。     這個內部函數就是每次調用執行的函數 - getImgInPositionedDivHtml( … ) -    */    return (function(url, id, width, height, top, left, altText){        /* 將不同的參數插入到緩衝數組相應的位置:*/        buffAr[1] = id;        buffAr[3] = top;        buffAr[5] = left;        buffAr[13] = (buffAr[7] = width);        buffAr[15] = (buffAr[9] = height);        buffAr[11] = url;        buffAr[17] = altText;        /* 返回通過使用空字符串(相當於將數組元素連接起來) 連接數組每個元素後形成的字符串:        */        return buffAr.join(”);    }); //:內部函數表達式結束。})();/*^^- :單行外部函數表達式。*/
如果一個函數依賴於另一(或多)個其他函數,而其他函數又沒有必要被其他代碼直接調用,那麼可以運用相同的技術來包裝這些函數,而通過一個公開暴露的函數來調用它們。這樣,就將一個複雜的多函數處理過程封裝成了一個具有移植性的代碼單元。
其他例子
有關閉包的一個可能是最廣爲人知的應用是 Douglas Crockford’s technique for the emulation of private instance variables in ECMAScript objects。這種應用方式可以擴展到各種嵌套包含的可訪問性(或可見性)的作用域結構,包括 the emulation of private static members for ECMAScript objects。
閉包可能的用途是無限的,可能理解其工作原理纔是把握如何使用它的最好指南。
意外的閉包
在創建可訪問的內部函數的函數體之外解析該內部函數就會構成閉包。這表明閉包很容易創建,但這樣一來可能會導致一種結果,即沒有認識到閉包是一種語言特性的 JavaScript 作者,會按照內部函數能完成多種任務的想法來使用內部函數。但他們對使用內部函數的結果並不明瞭,而且根本意識不到創建了閉包,或者那樣做意味着什麼。
正如下一節談到 IE 中內存泄漏問題時所提及的,意外創建的閉包可能導致嚴重的負面效應,而且也會影響到代碼的性能。問題不在於閉包本身,如果能夠真正做到謹慎地使用它們,反而會有助於創建高效的代碼。換句話說,使用內部函數會影響到效率。
使用內部函數最常見的一種情況就是將其作爲 DOM 元素的事件處理器。例如,下面的代碼用於向一個鏈接元素添加 onclick 事件處理器:
/* 定義一個全局變量,通過下面的函數將它的值   作爲查詢字符串的一部分添加到鏈接的 - href - 中:*/var quantaty = 5;/* 當給這個函數傳遞一個鏈接(作爲函數中的參數 - linkRef -)時,   會將一個 onclick 事件處理器指定給該鏈接,該事件處理器   將全局變量 - quantaty - 的值作爲字符串添加到鏈接的 - href -   屬性中,然後返回 true 使該鏈接在單擊後定位到由  - href -   屬性包含的查詢字符串指定的資源:*/function addGlobalQueryOnClick(linkRef){    /* 如果可以將參數 - linkRef - 通過類型轉換爲 ture      (說明它引用了一個對象):    */    if(linkRef){        /* 對一個函數表達式求值,並將對該函數對象的引用           指定給這個鏈接元素的 onclick 事件處理器:        */        linkRef.onclick = function(){            /* 這個內部函數表達式將查詢字符串               添加到附加事件處理器的元素的 - href - 屬性中:            */            this.href += (’?quantaty=’+escape(quantaty));            return true;        };    }}
無論什麼時候調用 addGlobalQueryOnClick 函數,都會創建一個新的內部函數(通過賦值構成了閉包)。從效率的角度上看,如果只是調用一兩次 addGlobalQueryOnClick 函數並沒有什麼大的妨礙,但如果頻繁使用該函數,就會導致創建許多截然不同的函數對象(每對內部函數表達式求一次值,就會產生一個新的函數對象)。
上面例子中的代碼沒有關注內部函數在創建它的函數外部可以訪問(或者說構成了閉包)這一事實。實際上,同樣的效果可以通過另一種方式來完成。即單獨地定義一個用於事件處理器的函數,然後將該函數的引用指定給元素的事件處理屬性。這樣,只需創建一個函數對象,而所有使用相同事件處理器的元素都可以共享對這個函數的引用:
/* 定義一個全局變量,通過下面的函數將它的值   作爲查詢字符串的一部分添加到鏈接的 - href - 中:*/var quantaty = 5;/* 當把一個鏈接(作爲函數中的參數 - linkRef -)傳遞給這個函數時,   會給這個鏈接添加一個 onclick 事件處理器,該事件處理器會   將全局變量  - quantaty - 的值作爲查詢字符串的一部分添加到   鏈接的 - href -  中,然後返回 true,以便單擊鏈接時定位到由   作爲 - href - 屬性值的查詢字符串所指定的資源:*/function addGlobalQueryOnClick(linkRef){    /* 如果 - linkRef - 參數能夠通過類型轉換爲 true    (說明它引用了一個對象):    */    if(linkRef){        /* 將一個對全局函數的引用指定給這個鏈接           的事件處理屬性,使函數成爲鏈接元素的事件處理器:        */        linkRef.onclick = forAddQueryOnClick;    }}/* 聲明一個全局函數,作爲鏈接元素的事件處理器,   這個函數將一個全局變量的值作爲要添加事件處理器的   鏈接元素的  - href - 值的一部分:*/function forAddQueryOnClick(){    this.href += (’?quantaty=’+escape(quantaty));    return true;}
在上面例子的第一個版本中,內部函數並沒有作爲閉包發揮應有的作用。在那種情況下,反而是不使用閉包更有效率,因爲不用重複創建許多本質上相同的函數對象。
類似地考量同樣適用於對象的構造函數。與下面代碼中的構造函數框架類似的代碼並不罕見:
function ExampleConst(param){    /* 通過對函數表達式求值創建對象的方法,      並將求值所得的函數對象的引用賦給要創建對象的屬性:    */    this.method1 = function(){        … // 方法體。    };    this.method2 = function(){        … // 方法體。    };    this.method3 = function(){        … // 方法體。    };    /* 把構造函數的參數賦給對象的一個屬性:*/    this.publicProp = param;}
每當通過 new ExampleConst(n) 使用這個構造函數創建一個對象時,都會創建一組新的、作爲對象方法的函數對象。因此,創建的對象實例越多,相應的函數對象也就越多。
Douglas Crockford 提出的模仿 JavaScript 對象私有成員的技術,就利用了將對內部函數的引用指定給在構造函數中構造對象的公共屬性而形成的閉包。如果對象的方法沒有利用在構造函數中形成的閉包,那麼在實例化每個對象時創建的多個函數對象,會使實例化過程變慢,而且將有更多的資源被佔用,以滿足創建更多函數對象的需要。
這那種情況下,只創建一次函數對象,並把它們指定給構造函數 prototype 的相應屬性顯然更有效率。這樣一來,它們就能被構造函數創建的所有對象共享了:
function ExampleConst(param){    /* 將構造函數的參數賦給對象的一個屬性:*/    this.publicProp = param;}/* 通過對函數表達式求值,並將結果函數對象的引用      指定給構造函數原型的相應屬性來創建對象的方法:*/ExampleConst.prototype.method1 = function(){    … // 方法體。};ExampleConst.prototype.method2 = function(){    … // 方法體。};ExampleConst.prototype.method3 = 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 中避免此類問題的關鍵。
閉包的兩個特點:
1、作爲一個函數變量的一個引用 - 當函數返回時,其處於激活狀態。
2、一個閉包就是當一個函數返回時,一個沒有釋放資源的棧區。
例1。
<script type="text/javascript">
function sayHello2(name) {
 var text = ''Hello '' + name; // local variable
 var sayAlert = function() { alert(text); }
 return sayAlert;
}
var sy = sayHello2(''never-online'');
sy();
</script>
作爲一個Javascript程序員,應該明白上面的代碼就是一個函數的引用。如果你還不明白或者不清楚的話,請先了解一些基本的知識,我這裏不再敘述。
上面的代碼爲什麼是一個閉包?
因爲sayHello2函數裏有一個內嵌匿名函數
sayAlert = function(){ alert(text); }
在Javascript裏。如果你創建了一個內嵌函數(如上例),也就是創建了一個閉包。
在C或者其它的主流語言中,當一個函數返回後,所有的局部變量將不可訪問,因爲它們所在的棧已經被消毀。但在Javascript裏,如果你聲明瞭一個內嵌函數,局部變量將在函數返回後依然可訪問。比如上例中的變量sy,就是引用內嵌函數中的匿名函數function(){ alert(text); },可以把上例改成這樣:
<script type="text/javascript">
function sayHello2(name) {
 var text = ''Hello '' + name; // local variable
 var sayAlert = function() { alert(text); }
 return sayAlert;
}
var sy = sayHello2(''never-online'');
alert(sy.toString());
</script>
這裏也就與閉包的第二個特點相吻合。
例2。
<script type="text/javascript">
function say667() {
 // Local variable that ends up within closure
 var num = 666;
 var sayAlert = function() { alert(num); }
 num++;
 return sayAlert;
}
var sy = say667();
sy();
alert(sy.toString());
</script>
上面的代碼中,匿名變量function() { alert(num); }中的num,並不是被拷貝,而是繼續引用外函數定義的局部變量——num中的值,直到外函數say667()返回。
例3。
<script type="text/javascript">
function setupSomeGlobals() {
 // Local variable that ends up within closure
 var num = 666;
 // Store some references to functions as global variables
 gAlertNumber = function() { alert(num); }
 gIncreaseNumber = function() { num++; }
 gSetNumber = function(x) { num = x; }
}
</script>
<button - setupSomeGlobals()</button>
<button - gAlertNumber()</button>
<button - gIncreaseNumber()</button>
<button - gSetNumber(5)</button>
上例中,gAlertNumber, gIncreaseNumber, gSetNumber都是同一個閉包的引用,setupSomeGlobals(),因爲他們聲明都是通過同一個全局調用——setupSomeGlobals()。
你可以通過“生成”,“增加”,“賦值”,“輸出值”這三個按扭來查看輸出結果。如果你點擊“生成”按鈕,將創建一個新閉包。也就會重寫gAlertNumber(), gIncreaseNumber(), gSetNumber(5)這三個函數。
如果理解以上代碼後,看下面的例子:
例4。
<script type="text/javascript">
function buildList(list) {
 var result = [];
 for (var i = 0; i < list.length; i++) {
 var item = ''item'' + list[i];
 result.push( function() {alert(item + '' '' + list[i])} );
 }
 return result;
}
function testList() {
 var fnlist = buildList([1,2,3]);
 // using j only to help prevent confusion - could use i
 for (var j = 0; j < fnlist.length; j++) {
 fnlist[j]();
 }
}
testList();
</script>
運行結果:
item 3 is undefined
item 3 is undefined
item 3 is undefined
代碼result.push( function() {alert(item + '' '' + list[i])} ),
使result數組添加了三個匿名函數的引用。這句代碼也可以寫成
var p = function() {alert(item + '' '' + list[i])};
result.push(p);

關於爲什麼會輸出三次都是 "item 3 is undefined"
在上面的例子say667()例子中已經解釋過了。
匿名函數function() {alert(item + '' '' + list[i])}中的list[i]並不是經過拷貝,而是對參數list的一個引用。直到函數buildList()返回爲止,也就是說,返回最後一個引用。
即遍歷完list(注:list的最大下標應該是2)後,經過i++也就變成了3,這也就是爲什麼是item 3,而list[3]本身是沒有初始化的,自然也就是undefined了。
例5。
<script type="text/javascript">
function newClosure(someNum, someRef) {
 // Local variables that end up within closure
 var num = someNum;
 var anArray = [1,2,3];
 var ref = someRef;
 return function(x) {
 num += x;
 anArray.push(num);
 alert(''num: '' + num + 
 ''nanArray '' + anArray.toString() + 
 ''nref.someVar '' + ref.someVar);
 }
}
var closure1 = newClosure(40, {someVar:'' never-online''})
var closure2 = newClosure(99, {someVar:'' BlueDestiny''})
closure1(4)
closure2(3)
</script>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章