深刻理解javascript閉包【整理】

原文連接:http://www.cn-cuckoo.com/2007/08/01/understand-javascript-closures-72.htmljavascript

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

要成爲高級 JavaScript 程序猿,就必須理解閉包。前端

本文結合 ECMA 262 規範具體解釋了閉包的內部工做機制。讓 JavaScript 編程人員對閉包的理解從「嵌套的函數」深刻到「標識符解析、執行環境和做用域鏈」等等 JavaScript 對象背後的執行機制其中。真正領會到閉包的實質。java

簡單介紹

Closure
所謂「閉包」,指的是一個擁有不少變量和綁定了這些變量的環境的表達式(一般是一個函數),於是這些變量也是該表達式的一部分。

「閉包」是一個表達式(一般是函數),它具備自由變量以及綁定這些變量的環境(該環境「封閉了」這個表達式)。
(閉包。就是封閉了外部函數做用域中變量的內部函數。但是。假設外部函數不返回這個內部函數。閉包的特性沒法顯現。假設外部函數返回這個內部函數。那麼返回的內部函數就成了名副事實上的閉包。此時,閉包封閉的外部變量就是自由變量,而由於該自由變量存在。外部函數即使返回,其佔用的內存也得不到釋放。

——譯者注,2010年4月3日)node

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

關於閉包。最簡單的描寫敘述就是 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 keyword而賦值。

假設所賦的值引用一個對象,那麼前綴以 this keyword的屬性訪問器就是引用該對象的屬性。

假設所賦(內部)值是 null,那麼 this keyword則引用全局對象。

建立全局運行環境的過程會稍有不一樣,因爲它沒有參數,因此不需要經過定義的活動對象來引用這些參數。但全局運行環境也需要一個做用域,而它的做用域鏈實際上僅僅由一個對象--全局對象--組成。全局運行環境也會有變量實例化的過程。它的內部函數就是涉及大部分 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

例 3:包裝相關的功能

閉包可以用於建立額外的做用域,經過該做用域可以將相關的和具備依賴性的代碼組織起來。以便將意外交互的風險降到最低。若是有一個用於構建字符串的函數,爲了不反覆性的鏈接操做(和建立衆多的中間字符串),咱們的願望是使用一個數組按順序來存儲字符串的各個部分,而後再使用 Array.prototype.join 方法(以空字符串做爲其參數)輸出結果。這個數組將做爲輸出的緩衝器,但是將數組做爲函數的局部變量又會致使在每次調用函數時都又一次建立一個新數組,這在每次調用函數時僅僅又一次指定數組中的可變內容的狀況下並不是必要的。

一種解決方式是將這個數組聲明爲全局變量,這樣就可以重用這個數組,而沒必要每次都創建新數組。

但這個方法的結果是,除了引用函數的全局變量會使用這個緩衝數組外,還會多出一個全局屬性引用數組自身。如此不只使代碼變得不easy管理。而且,假設要在其它地方使用這個數組時,開發人員必須要再次定義函數和數組。

這樣一來,也使得代碼不easy與其它代碼整合,因爲此時不只要保證所使用的函數名在全局命名空間中是惟一的,而且還要保證函數所依賴的數組在全局命名空間中也必須是惟一的。

而經過閉包可使做爲緩衝器的數組與依賴它的函數關聯起來(優雅地打包),同一時候也能夠維持在全局命名空間外指定的緩衝數組的屬性名,免除了名稱衝突和意外交互的危急。

當中的關鍵技巧在於經過運行一個單行(in-line)函數表達式建立一個額外的運行環境,而將該函數表達式返回的內部函數做爲在外部代碼中使用的函數。此時,緩衝數組被定義爲函數表達式的一個局部變量。

這個函數表達式僅僅需運行一次,而數組也僅僅需建立一次,就可以供依賴它的函數反覆使用。

如下的代碼定義了一個函數,這個函數用於返回一個 HTML 字符串,當中大部份內容都是常量。但這些常量字符序列中需要穿插一些可變的信息。而可變的信息由調用函數時傳遞的參數提供。

經過運行單行函數表達式返回一個內部函數,並將返回的函數賦給一個全局變量,所以這個函數也可以稱爲全局函數。

而緩衝數組被定義爲外部函數表達式的一個局部變量。它不會暴露在全局命名空間中。而且無論何時調用依賴它的函數都不需要又一次建立這個數組。

/* 聲明一個全局變量 - getImgInPositionedDivHtml - 並將一次調用一個外部函數表達式返回的內部函數賦給它。 這個內部函數會返回一個用於表示絕對定位的 DIV 元素 包圍着一個 IMG 元素 的 HTML 字符串,這樣一來, 所有可變的屬性值都由調用該函數時的參數提供: */ 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

閉包可能的用途是無限的。可能理解其工做原理纔是把握怎樣使用它的最好指南。

意外的閉包

在建立可訪問的內部函數的函數體以外解析該內部函數就會構成閉包。這代表閉包很是easy建立,但這樣一來可能會致使一種結果。即沒有認識到閉包是一種語言特性的 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 對象而言,僅僅要沒有其它對象引用對象 一、二、3。也就是說它們僅僅是相互之間的引用,那麼仍然會被垃圾收集系統識別並處理。但是。在 Internet Explorer 中。假設循環引用中的不論什麼對象是 DOM 節點或者 ActiveX 對象,垃圾收集系統則不會發現它們之間的循環關係與系統中的其它對象是隔離的並釋放它們。終於它們將被保留在內存中,直到瀏覽器關閉。

閉包很easy構成循環引用。

假設一個構成閉包的函數對象被指定給,比方一個 DOM 節點的事件處理器,而對該節點的引用又被指定給函數對象做用域中的一個活動(或可變)對象,那麼就存在一個循環引用。DOM_Node.onevent ->function_object.[[scope]] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。造成這樣一個循環引用是垂手可得的。而且略微瀏覽一下包括相似循環引用代碼的站點(通常會出現在站點的每個頁面中),就會消耗大量(甚至全部)系統內存。

多加註意可以避免造成循環引用,而在沒法避免時,也可以使用補償的方法。比方使用 IE 的 onunload 事件來來清空(null)事件處理函數的引用。時刻意識到這個問題並理解閉包的工做機制是在 IE 中避免此類問題的關鍵。

【注】本文內容講的很是深刻,貼出來,一是本身學習以備後查,二是分享給你們,回想一下基礎的知識。

順便多想一下。用了這麼長時間的javascript,你真的瞭解嗎?

相關文章
相關標籤/搜索