目錄簡介閉包是 ECMAScript (JavaScript)最強大的特性之一,但用好閉包的前提是必須理解閉包。閉包的創(chuàng)建相對容易,人們甚至會在不經意間創(chuàng)建閉包,但這些無意創(chuàng)建的閉包卻存在潛在的危害,尤其是在比較常見的瀏覽器環(huán)境下。如果想要揚長避短地使用閉包這一特性,則必須了解它們的工作機制。而閉包工作機制的實現(xiàn)很大程度上有賴于標識符(或者說對象屬性)解析過程中作用域的角色。 關于閉包,最簡單的描述就是 ECMAScript 允許使用內部函數--即函數定義和函數表達式位于另一個函數的函數體內。而且,這些內部函數可以訪問它們所在的外部函數中聲明的所有局部變量、參數和聲明的其他內部函數。當其中一個這樣的內部函數在包含它們的外部函數之外被調用時,就會形成閉包。也就是說,內部函數會在外部函數返回后被執(zhí)行。而當這個內部函數執(zhí)行時,它仍然必需訪問其外部函數的局部變量、參數以及其他內部函數。這些局部變量、參數和函數聲明(最初時)的值是外部函數返回時的值,但也會受到內部函數的影響。 遺憾的是,要適當地理解閉包就必須理解閉包背后運行的機制,以及許多相關的技術細節(jié)。雖然本文的前半部分并沒有涉及 ECMA 262 規(guī)范指定的某些算法,但仍然有許多無法回避或簡化的內容。對于個別熟悉對象屬性名解析的人來說,可以跳過相關的內容,但是除非你對閉包也非常熟悉,否則最好是不要跳下面幾節(jié)。 對象屬性名解析ECMAScript 認可兩類對象:原生(Native)對象和宿主(Host)對象,其中宿主對象包含一個被稱為內置對象的原生對象的子類(ECMA 262 3rd Ed Section 4.3)。原生對象屬于語言,而宿主對象由環(huán)境提供,比如說可能是文檔對象、DOM 等類似的對象。 原生對象具有松散和動態(tài)的命名屬性(對于某些實現(xiàn)的內置對象子類別而言,動態(tài)性是受限的--但這不是太大的問題)。對象的命名屬性用于保存值,該值可以是指向另一個對象(Objects)的引用(在這個意義上說,函數也是對象),也可以是一些基本的數據類型,比如:String、Number、Boolean、Null 或 Undefined。其中比較特殊的是 Undefined 類型,因為可以給對象的屬性指定一個 Undefined 類型的值,而不會刪除對象的相應屬性。而且,該屬性只是保存著 undefined 值。 下面簡要介紹一下如何設置和讀取對象的屬性值,并最大程度地體現(xiàn)相應的內部細節(jié)。 值的賦予對象的命名屬性可以通過為該命名屬性賦值來創(chuàng)建,或重新賦值。即,對于: var objectRef = new Object(); //創(chuàng)建一個普通的 JavaScript 對象。 可以通過下面語句來創(chuàng)建名為 “testNumber” 的屬性: objectRef.testNumber = 5; 在賦值之前,對象中沒有“testNumber” 屬性,但在賦值后,則創(chuàng)建一個屬性。之后的任何賦值語句都不需要再創(chuàng)建這個屬性,而只會重新設置它的值: objectRef.testNumber = 8; 稍后我們會介紹,Javascript 對象都有原型(prototypes)屬性,而這些原型本身也是對象,因而也可以帶有命名的屬性。但是,原型對象命名屬性的作用并不體現(xiàn)在賦值階段。同樣,在將值賦給其命名屬性時,如果對象沒有該屬性則會創(chuàng)建該命名屬性,否則會重設該屬性的值。 值的讀取當讀取對象的屬性值時,原型對象的作用便體現(xiàn)出來。如果對象的原型中包含屬性訪問器(property accessor)所使用的屬性名,那么該屬性的值就會返回: /* 為命名屬性賦值。如果在賦值前對象沒有相應的屬性,那么賦值后就會得到一個:*/ /* 從屬性中讀取值 */ /* 現(xiàn)在, - val - 中保存著剛賦給對象命名屬性的值 8*/ 而且,由于所有對象都有原型,而原型本身也是對象,所以原型也可能有原型,這樣就構成了所謂的原型鏈。原型鏈終止于鏈中原型為 null 的對象。 var objectRef = new Object(); //創(chuàng)建一個普通的 JavaScript 對象。 創(chuàng)建了一個原型為 /* 創(chuàng)建 - MyObject1 - 類型對象的函數*/ /* 接下來的操作用 MyObject1 類的實例替換了所有與 MyObject2 類的實例相關聯(lián)的原型。而且,為 MyObject1 構造函數傳遞了參數 - 8 - ,因而其 - testNumber - 屬性被賦予該值:*/ /* 最后,將一個字符串作為構造函數的第一個參數,創(chuàng)建一個 - MyObject2 - 的實例,并將指向該對象的引用賦給變量 - objectRef - :*/ 被變量 當某個屬性訪問器嘗試讀取由 var val = objectRef.testString; 因為 var val = objectRef.testNumber; 則不能從 var val = objectRef.toString; 變量 最后: var val = objectRef.madeUpProperty; 返回 不論是在對象或對象的原型中,讀取命名屬性值的時候只返回首先找到的屬性值。而當為對象的命名屬性賦值時,如果對象自身不存在該屬性則創(chuàng)建相應的屬性。 這意味著,如果執(zhí)行像 注意:ECMAScript 為 Object 類型定義了一個內部 標識符解析、執(zhí)行環(huán)境和作用域鏈執(zhí)行環(huán)境執(zhí)行環(huán)境是 ECMAScript 規(guī)范(ECMA 262 第 3 版)用于定義 ECMAScript 實現(xiàn)必要行為的一個抽象的概念。對如何實現(xiàn)執(zhí)行環(huán)境,規(guī)范沒有作規(guī)定。但由于執(zhí)行環(huán)境中包含引用規(guī)范所定義結構的相關屬性,因此執(zhí)行環(huán)境中應該保有(甚至實現(xiàn))帶有屬性的對象--即使屬性不是公共屬性。 所有 JavaScript 代碼都是在一個執(zhí)行環(huán)境中被執(zhí)行的。全局代碼(作為內置的JS 文件執(zhí)行的代碼,或者 當調用一個 JavaScript 函數時,該函數就會進入相應的執(zhí)行環(huán)境。如果又調用了另外一個函數(或者遞歸地調用同一個函數),則又會創(chuàng)建一個新的執(zhí)行環(huán)境,并且在函數調用期間執(zhí)行過程都處于該環(huán)境中。當調用的函數返回后,執(zhí)行過程會返回原始執(zhí)行環(huán)境。因而,運行中的 JavaScript 代碼就構成了一個執(zhí)行環(huán)境棧。 在創(chuàng)建執(zhí)行環(huán)境的過程中,會按照定義的先后順序完成一系列操作。首先,在一個函數的執(zhí)行環(huán)境中,會創(chuàng)建一個“活動”對象。活動對象是規(guī)范中規(guī)定的另外一種機制。之所以稱之為對象,是因為它擁有可訪問的命名屬性,但是它又不像正常對象那樣具有原型(至少沒有預定義的原型),而且不能通過 JavaScript 代碼直接引用活動對象。 為函數調用創(chuàng)建執(zhí)行環(huán)境的下一步是創(chuàng)建一個 接著,為執(zhí)行環(huán)境分配作用域。作用域由對象列表(鏈)組成。每個函數對象都有一個內部的 之后會發(fā)生由 ECMA 262 中所謂“可變”對象完成的“變量實例化”的過程。只不過此時使用活動對象作為可變對象(這里很重要,請注意:它們是同一個對象)。此時會將函數的形式參數創(chuàng)建為可變對象的命名屬性,如果調用函數時傳遞的參數與形式參數一致,則將相應參數的值賦給這些命名屬性(否則,會給命名屬性賦 根據聲明的局部變量創(chuàng)建的可變對象的屬性在變量實例化過程中會被賦予 事實上,擁有 最后,要為使用 創(chuàng)建全局執(zhí)行環(huán)境的過程會稍有不同,因為它沒有參數,所以不需要通過定義的活動對象來引用這些參數。但全局執(zhí)行環(huán)境也需要一個作用域,而它的作用域鏈實際上只由一個對象--全局對象--組成。全局執(zhí)行環(huán)境也會有變量實例化的過程,它的內部函數就是涉及大部分 JavaScript 代碼的、常規(guī)的頂級函數聲明。而且,在變量實例化過程中全局對象就是可變對象,這就是為什么全局性聲明的函數是全局對象屬性的原因。全局性聲明的變量同樣如此。 全局執(zhí)行環(huán)境也會使用 作用域鏈與 [[scope]]調用函數時創(chuàng)建的執(zhí)行環(huán)境會包含一個作用域鏈,這個作用域鏈是通過將該執(zhí)行環(huán)境的活動(可變)對象添加到保存于所調用函數對象的 在 ECMAScript 中,函數也是對象。函數對象在變量實例化過程中會根據函數聲明來創(chuàng)建,或者是在計算函數表達式或調用 通過調用 通過函數聲明或函數表達式創(chuàng)建的函數對象,其內部的 在最簡單的情況下,比如聲明如下全局函數:- function exampleFunction(formalParameter){ - 當為創(chuàng)建全局執(zhí)行環(huán)境而進行變量實例化時,會根據上面的函數聲明創(chuàng)建相應的函數對象。因為全局執(zhí)行環(huán)境的作用域鏈中只包含全局對象,所以它就給自己創(chuàng)建的、并以名為“exampleFunction”的屬性引用的這個函數對象的內部 當在全局環(huán)境中計算函數表達式時,也會發(fā)生類似的指定作用域鏈的過程:- var exampleFuncRef = function(){ 在這種情況下,不同的是在全局執(zhí)行環(huán)境的變量實例化過程中,會先為全局對象創(chuàng)建一個命名屬性。而在計算賦值語句之前,暫時不會創(chuàng)建函數對象,也不會將該函數對象的引用指定給全局對象的命名屬性。但是,最終還是會在全局執(zhí)行環(huán)境中創(chuàng)建這個函數對象(當計算函數表達式時。譯者注),而為這個創(chuàng)建的函數對象的
exampleFuncWith();在調用 當與 例 3:包裝相關的功能閉包可以用于創(chuàng)建額外的作用域,通過該作用域可以將相關的和具有依賴性的代碼組織起來,以便將意外交互的風險降到最低。假設有一個用于構建字符串的函數,為了避免重復性的連接操作(和創(chuàng)建眾多的中間字符串),我們的愿望是使用一個數組按順序來存儲字符串的各個部分,然后再使用 一種解決方案是將這個數組聲明為全局變量,這樣就可以重用這個數組,而不必每次都建立新數組。但這個方案的結果是,除了引用函數的全局變量會使用這個緩沖數組外,還會多出一個全局屬性引用數組自身。如此不僅使代碼變得不容易管理,而且,如果要在其他地方使用這個數組時,開發(fā)者必須要再次定義函數和數組。這樣一來,也使得代碼不容易與其他代碼整合,因為此時不僅要保證所使用的函數名在全局命名空間中是唯一的,而且還要保證函數所依賴的數組在全局命名空間中也必須是唯一的。 而通過閉包可以使作為緩沖器的數組與依賴它的函數關聯(lián)起來(優(yōu)雅地打包),同時也能夠維持在全局命名空間外指定的緩沖數組的屬性名,免除了名稱沖突和意外交互的危險。 其中的關鍵技巧在于通過執(zhí)行一個單行(in-line)函數表達式創(chuàng)建一個額外的執(zhí)行環(huán)境,而將該函數表達式返回的內部函數作為在外部代碼中使用的函數。此時,緩沖數組被定義為函數表達式的一個局部變量。這個函數表達式只需執(zhí)行一次,而數組也只需創(chuàng)建一次,就可以供依賴它的函數重復使用。 下面的代碼定義了一個函數,這個函數用于返回一個 HTML 字符串,其中大部分內容都是常量,但這些常量字符序列中需要穿插一些可變的信息,而可變的信息由調用函數時傳遞的參數提供。 通過執(zhí)行單行函數表達式返回一個內部函數,并將返回的函數賦給一個全局變量,因此這個函數也可以稱為全局函數。而緩沖數組被定義為外部函數表達式的一個局部變量。它不會暴露在全局命名空間中,而且無論什么時候調用依賴它的函數都不需要重新創(chuàng)建這個數組。
如果一個函數依賴于另一(或多)個其他函數,而其他函數又沒有必要被其他代碼直接調用,那么可以運用相同的技術來包裝這些函數,而通過一個公開暴露的函數來調用它們。這樣,就將一個復雜的多函數處理過程封裝成了一個具有移植性的代碼單元。 其他例子有關閉包的一個可能是最廣為人知的應用是 Douglas Crockford’s technique for the emulation of private instance variables in ECMAScript objects。這種應用方式可以擴展到各種嵌套包含的可訪問性(或可見性)的作用域結構,包括 the emulation of private static members for ECMAScript objects。 閉包可能的用途是無限的,可能理解其工作原理才是把握如何使用它的最好指南。 意外的閉包在創(chuàng)建可訪問的內部函數的函數體之外解析該內部函數就會構成閉包。這表明閉包很容易創(chuàng)建,但這樣一來可能會導致一種結果,即沒有認識到閉包是一種語言特性的 JavaScript 作者,會按照內部函數能完成多種任務的想法來使用內部函數。但他們對使用內部函數的結果并不明了,而且根本意識不到創(chuàng)建了閉包,或者那樣做意味著什么。 正如下一節(jié)談到 IE 中內存泄漏問題時所提及的,意外創(chuàng)建的閉包可能導致嚴重的負面效應,而且也會影響到代碼的性能。問題不在于閉包本身,如果能夠真正做到謹慎地使用它們,反而會有助于創(chuàng)建高效的代碼。換句話說,使用內部函數會影響到效率。 使用內部函數最常見的一種情況就是將其作為 DOM 元素的事件處理器。例如,下面的代碼用于向一個鏈接元素添加 onclick 事件處理器:
無論什么時候調用 上面例子中的代碼沒有關注內部函數在創(chuàng)建它的函數外部可以訪問(或者說構成了閉包)這一事實。實際上,同樣的效果可以通過另一種方式來完成。即單獨地定義一個用于事件處理器的函數,然后將該函數的引用指定給元素的事件處理屬性。這樣,只需創(chuàng)建一個函數對象,而所有使用相同事件處理器的元素都可以共享對這個函數的引用:
在上面例子的第一個版本中,內部函數并沒有作為閉包發(fā)揮應有的作用。在那種情況下,反而是不使用閉包更有效率,因為不用重復創(chuàng)建許多本質上相同的函數對象。 類似地考量同樣適用于對象的構造函數。與下面代碼中的構造函數框架類似的代碼并不罕見:
每當通過 Douglas Crockford 提出的模仿 JavaScript 對象私有成員的技術,就利用了將對內部函數的引用指定給在構造函數中構造對象的公共屬性而形成的閉包。如果對象的方法沒有利用在構造函數中形成的閉包,那么在實例化每個對象時創(chuàng)建的多個函數對象,會使實例化過程變慢,而且將有更多的資源被占用,以滿足創(chuàng)建更多函數對象的需要。 這那種情況下,只創(chuàng)建一次函數對象,并把它們指定給構造函數
Internet Explorer 的內存泄漏問題Internet Explorer Web 瀏覽器(在 IE 4 到 IE 6 中核實)的垃圾收集系統(tǒng)中存在一個問題,即如果 ECMAScript 和某些宿主對象構成了 “循環(huán)引用”,那么這些對象將不會被當作垃圾收集。此時所謂的宿主對象指的是任何 DOM 節(jié)點(包括 document 對象及其后代元素)和 ActiveX 對象。如果在一個循環(huán)引用中包含了一或多個這樣的對象,那么這些對象直到瀏覽器關閉都不會被釋放,而它們所占用的內存同樣在瀏覽器關閉之前都不會交回系統(tǒng)重用。 當兩個或多個對象以首尾相連的方式相互引用時,就構成了循環(huán)引用。比如對象 1 的一個屬性引用了對象 2 ,對象 2 的一個屬性引用了對象 3,而對象 3 的一個屬性又引用了對象 1。對于純粹的 ECMAScript 對象而言,只要沒有其他對象引用對象 1、2、3,也就是說它們只是相互之間的引用,那么仍然會被垃圾收集系統(tǒng)識別并處理。但是,在 Internet Explorer 中,如果循環(huán)引用中的任何對象是 DOM 節(jié)點或者 ActiveX 對象,垃圾收集系統(tǒng)則不會發(fā)現(xiàn)它們之間的循環(huán)關系與系統(tǒng)中的其他對象是隔離的并釋放它們。最終它們將被保留在內存中,直到瀏覽器關閉。 閉包非常容易構成循環(huán)引用。如果一個構成閉包的函數對象被指定給,比如一個 DOM 節(jié)點的事件處理器,而對該節(jié)點的引用又被指定給函數對象作用域中的一個活動(或可變)對象,那么就存在一個循環(huán)引用。DOM_Node.onevent ->function_object.[[scope]] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。形成這樣一個循環(huán)引用是輕而易舉的,而且稍微瀏覽一下包含類似循環(huán)引用代碼的網站(通常會出現(xiàn)在網站的每個頁面中),就會消耗大量(甚至全部)系統(tǒng)內存。 多加注意可以避免形成循環(huán)引用,而在無法避免時,也可以使用補償的方法,比如使用 IE 的 onunload 事件來來清空(null)事件處理函數的引用。時刻意識到這個問題并理解閉包的工作機制是在 IE 中避免此類問題的關鍵。 |
|
|
來自: bluecrystal > 《JavaScript》