再接著看函數——具有魔幻色彩的對象。
9、作為值的函數
在一般的程式設計語言中,如果要將函數作為值來使用,需要使用類似函數指標或者代理的方式來實現,但是在ECMAScript中,函數是一種對象,擁有一般對象具有的所有特徵,除了函數可以有自己的屬性和方法外,還可以做為一個參考型別的值去使用,實際上我們前面的例子中已經有過將函數作為一個對象屬性的值,又比如函數也可以作為另一個函數的參數或者傳回值,非同步處理中的回呼函數就是一個典型的用法。
複製代碼 代碼如下:var name = 'linjisong';
var person = {name:'oulinhai'};
function getName(){
return this.name;
}
function sum(){
var total = 0,
l = arguments.length;
for(; l; l--)
{
total += arguments[l-1];
}
return total;
}
// 定義調用函數的函數,使用函數作為形式參數
function callFn(fn,arguments,scope){
arguments = arguments || [];
scope = scope || window;
return fn.apply(scope, arguments);
}
// 調用callFn,使用函數作為實際參數
console.info(callFn(getName));//linjisong
console.info(callFn(getName,'',person));//oulinhai
console.info(callFn(sum,[1,2,3,4]));//10
再看一個使用函數作為傳回值的典型例子,這個例子出自於原書第5章: 複製代碼 代碼如下:function createComparisonFunction(propertyName) {
return function(object1, object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
};
}
var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}];
data.sort(createComparisonFunction("name"));
console.info(data[0].name); //Nicholas
data.sort(createComparisonFunction("age"));
console.info(data[0].name); //Zachary
10、閉包(Closure)
閉包是指有權訪問另一個函數範圍中的變數的函數。對象是帶函數的資料,而閉包是帶資料的函數。
首先閉包是一個函數,然後閉包是一個帶有資料的函數,那麼,帶有的是什麼資料呢?我們往上看看函數作為傳回值的例子,返回的是一個匿名函數,而隨著這個匿名函數被返回,外層的createComparisonFunction()函數代碼也就執行完成,按照前面的結論,外層函數的執行環境會被彈出棧並銷毀,但是接下來的排序中可以看到在返回的匿名函數中依舊可以訪問處於createComparisonFunction()範圍中的propertyName,這說明儘管createComparisonFunction()對應的執行環境已經被銷毀,但是這個執行環境相對應的使用中的物件並沒有被銷毀,而是作為返回的匿名函數的範圍鏈中的一個對象了,換句話說,返回的匿名函數構成的閉包帶有的資料就是:外層函數相應的使用中的物件。由於使用中的物件的屬性(也就是外層函數中定義的變數、函數和形式參數)會隨著外層函數的代碼執行而變化,因此最終返回的匿名函數構成的閉包帶有的資料是外層函數代碼執行完成之後的使用中的物件,也就是最終狀態。
希望好好理解一下上面這段話,反覆理解一下。雖然我已經盡我所能描述的更易於理解一些,但是閉包的概念還是有些抽象,下面看一個例子,這個例子來自原書第7章: 複製代碼 代碼如下:function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
var funcs = createFunctions();
for (var i=0,l=funcs.length; i < l; i++){
console.info(funcs[i]());//每一個函數都輸出10
}
這裡由於閉包帶有的資料是createFunctions相應的使用中的物件的最終狀態,而在createFunctions()代碼執行完成之後,使用中的物件的屬性i已經變成10,因此在下面的調用中每一個返回的函數都輸出10了,要處理這種問題,可以採用匿名函數範圍來儲存狀態: 複製代碼 代碼如下:function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = (function(num){
return function(){
return num;
};
})(i);
}
return result;
}
將每一個狀態都使用一個立即調用的匿名函數來儲存(儲存在匿名函數相應的使用中的物件中),然後在最終返回的函數被調用時,就可以通過閉包帶有的資料(相應的匿名函式活動對象中的資料)來正確訪問了,輸出結果變成0,1,...9。當然,這樣做,就建立了10個閉包,在效能上會有較大影響,因此建議不要濫用閉包,另外,由於閉包會儲存其它執行環境的使用中的物件作為自身範圍鏈中的一環,這也可能會造成記憶體泄露。儘管閉包存在效率和記憶體的隱患,但是閉包的功能是在太強大,下面就來看看閉包的應用——首先讓我們回到昨天所說的函數Binder 方法bind()。
(1)函數綁定與柯裡化(currying)
A、再看this,先看一個例子(原書第22章): 複製代碼 代碼如下:<button id='my-btn' title='Button'>Hello</button>
<script type="text/javascript">
var handler = {
title:'Event',
handleClick:function(event){
console.info(this.title);
}
};
var btn = document.getElementById('my-btn');//擷取頁面按鈕
btn.onclick = handler.handleClick;//給頁面按鈕添加事件處理函數
</script>
如果你去點擊“Hello”按鈕,控制台列印的是什麼呢?竟然是Button,而不是期望中的Event,原因就是這裡在點擊按鈕的時候,處理函數內部屬性this指向了按鈕對象。可以使用閉包來解決這個問題: 複製代碼 代碼如下:btn.onclick = function(event){
handler.handleClick(event);//形成一個閉包,調用函數的就是對象handler了,函數內部屬性this指向handler對象,因此會輸出Event}
B、上面的解決方案並不優雅,在ES5中新增了函數Binder 方法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.curry){
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 = (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版)》譯者的一篇譯文:命名函數運算式探秘。