首先閉包是一個函數,然後閉包是一個帶有資料的函數,那麼,帶有的是什麼資料呢?我們往上看看函數作為返回值的例子,返回的是一個匿名函數,而隨著這個匿名函數被返回,外層的createComparisonFunction()函數代碼也就執行完成,按照前面的結論,外層函數的執行環境會被彈出堆疊並銷毀,但是接下來的排序中可以看到在傳回的匿名函數中依舊可以存取處於createComparisonFunction()作用域中的propertyName,這說明儘管createComparisonFunction()對應的執行環境已經被銷毀,但是這個執行環境相對應的活動對象並沒有被銷毀,而是作為返回的匿名函數的作用域鏈中的一個對象了,換句話說,返回的匿名函數構成的閉包帶有的數據就是:外層函數對應的活動對象。由於活動物件的屬性(也就是外層函數中定義的變數、函數和形式參數)會隨著外層函數的程式碼執行而變化,因此最終傳回的匿名函數所構成的閉包帶有的資料是外層函數程式碼執行完成之後的活動對象,也就是最終狀態。
function createFunctions(){
var result = new Array(); ; i result[i] = function(){
return i;
};
}
return result;
}
var funcs = createFunctions();
for (var i=0,l=funcs.length; i console.info(funcs[i]());//每一個函數都輸出10
}
這裡由於閉包帶有的資料是createFunctions對應的活動物件的最終狀態,而在createFunctions()程式碼執行完成之後,活動物件的屬性i已經變成10,因此在下面的呼叫中每一個返回的函數都輸出10了,要處理這種問題,可以用匿名函數作用域來保存狀態:
function createFunctions(){ var result = new Array(); for (var i=0; i result[i ] = (function(num){ return function(){ return num; }; })(i); } return result; }
將每個狀態都使用一個立即呼叫的匿名函數來保存(保存在匿名函數對應的活動物件中),然後在最終返回的函數被呼叫時,就可以透過閉包帶有的資料(對應的匿名函數活動物件中的資料)來正確存取了,輸出結果變成0,1,...9。當然,這樣做,就創建了10個閉包,在性能上會有較大影響,因此建議不要濫用閉包,另外,由於閉包會保存其它執行環境的活動對像作為自身作用域鏈中的一環,這也可能會造成記憶體外洩。儘管閉包存在效率和內存的隱患,但是閉包的功能是在太強大,下面就來看看閉包的應用——首先讓我們回到昨天所說的函數綁定方法bind()。
(1)函數綁定與柯里化(currying)
A、再看this,先看一個例子(原書第22章):
Hello
如果你去點擊「Hello」按鈕,控制台列印的是什麼呢?竟然是Button,而不是期望中的Event,原因就是這裡在點擊按鈕的時候,處理函數內部屬性this指向了按鈕物件。可以用閉包來解決這個問題:
btn.onclick = function(event){ handler.handleClick(event);//形成一個閉包,呼叫函數的就是對象handler了,函數內部屬性this指向handler對象,因此會輸出Event}
B、上面的解決方案並不優雅,在ES5中新增了函數綁定方法bind(),我們用這個方法來改寫一下:
if(!Function.prototype.bind){//bind為ES5新增,為了確保運作正常,在不支援的瀏覽器上加入這個方法 Function.prototype.bind = function(scope){ var that = this;//呼叫bind()方法的函數物件 return function(){ that.apply(scope, arguments);//使用apply方法,指定that函數物件的內部屬性this }; }; } btn.onclick = handler.handleClick.bind( handler);//使用bind()方法時只需要使用一條語句即可
這裡新增的bind()方法中,主要技術也是建立一個閉包,保存綁定時的參數作為函數實際調用時的內部屬性this。如果你不確定是瀏覽器本身就支援bind()還是我們這裡的bind()起了作用,你可以把特性偵測的條件判斷去掉,然後換個方法名稱試試。
C、上面對函數使用bind()方法時,只使用了第一個參數,如果調用bind()時傳入多個參數並且將第2個參數開始作為函數實際調用時的參數,那我們就可以給函數綁定預設參數了。
if(!Function.prototype.bind){ Function.prototype.bind = function(scope){ var that = this;//呼叫bind()方法的函數物件 var args = Array.prototype.slice.call(arguments,1);//從第2個參數開始組成的參數數組 return function(){ var innerArgs = Array.prototype.slice.apply (arguments); that.apply(scope, args.concat(innerArgs));//使用apply方法,指定that函數物件的內部屬性this,並且填入綁定時傳入的參數 }; }; }
D、柯里化:在上面綁定時,第一個參數都是用來設定函數呼叫時的內部屬性this,如果把所有綁定時的參數都作為預填的參數,則稱為函數柯里化。
if(!Function.prototype.curury){ if(!Function.prototype.curi] >Function.prototype.curry = function(){ var that = this;//呼叫curry()方法的函數物件 var args = Array.prototype.slice.call(arguments);//預填參數陣列 return function(){ var innerArgs = Array.prototype.slice.apply(arguments);//實際呼叫時參數陣列 that.apply(this, args.concat(innerArgs)) ;//使用apply方法,並且加入預填的參數 }; }; }
(2)利用閉包快取
還記得前面使用遞歸實作斐波那契數列的函數嗎?使用閉包快取來改寫:
程式碼如下:
var fibonacci = ( var fibonacci = ( var fibonacci = ( function(){//使用閉包緩存,遞歸 var cache = []; function f(n){ if(1 == n || 2 == n){ return 1; }else{ cache[n] = cache[n] || (f(n-1) f(n-2)); return cache[n]; } } return f; })(); var f2 = function(n){//不使用閉包緩存,直接遞歸
if(1 = = n || 2 == n){
return 1;
}else{
return f2(n-1) f2(n-2);
} }; 以下是測試程式碼以及我機器上的運作結果:
複製程式碼 程式碼如下: var test = function(n){ var start = new Date().getTime(); console.info(fibonacci(n)); console.info( new Date().getTime() - start); start = new Date().getTime();
console.info(f2(n));
console.info(new Date().getTime() - start);
};
test(10);//55,2,55,2
test(20);//6765,1,6765,7
test(30);//832040,2,832040,643
可以看到,n值越大,使用快取運算的優勢越明顯。作為練習,你可以試著自己修改計算階乘的函數。 (3)模仿區塊級作用域 在ECMAScript中,有語句區塊,但是卻沒有對應的區塊級作用域,但我們可以使用閉包來模仿區塊級作用域,一般格式為:
複製程式碼
程式碼如下:
(function((){) >//這裡是區塊語句
})();
上面這種模式也稱為立即呼叫的函數表達式,這種模式已經非常流行了,特別是由於jQuery原始碼使用這種方式而大規模普及起來。 閉包還有很多有趣的應用,例如模仿私有變數和私有函數、模組模式等,這裡先不討論了,在深入理解物件之後再看這些內容。 關於函數,就先說這些,在網路上也有很多非常棒的文章,有興趣的可以自己搜尋一下閱讀。這裡推薦一篇文章,《JavaScript高級程式設計(第3版)》譯者的一篇譯文:命名函數表達式探針。