JavaScript高階函數的應用
定義
高階函數是指至少滿足下列條件之一的函數:
-
函數可以作為參數被傳遞;
-
函數可以作為返回值輸出。
JavaScript語言中的函數顯然滿足高階函數的條件,在實際開發中,無論是將函數當作參數傳遞,還是讓函數的執行結果返回另外一個函數,這兩種情形都有很多應用場景,以下就是一些高階函數的應用。
應用
一、作為參數傳遞
ajax異步請求
// callback為待傳入的回調函數
var getUserInfo = function(userId, callback) {
$.ajax("http://xxx.com/getUserInfo?" + userId, function(data) {
if (typeof callback === "function") {
callback(data);
}
});
}
getUserInfo(13157, function(data) {
alert (data.userName);
});
Array.prototype.sort
Array.prototype.sort接受一個函數當作參數,這個函數裏面封裝了數組元素的排序規則。從Array.prototype.sort的使用可以看到,我們的目的是對數組進行排序,這是不變的部分;而使用什麽規則去排序,則是可變的部分。把可變的部分封裝在函數參數裏,動態傳入Array.prototype.sort,使Array.prototype.sort方法成為了一個非常靈活的方法。
//從小到大排列
[1, 4, 3].sort(function(a, b) {
return a - b;
});
// 輸出: [1, 3, 4]
//從大到小排列
[1, 4, 3].sort(function(a, b) {
return b - a;
});
// 輸出: [4, 3, 1]
類似的數組方法還有map(),reduce(),filter(),詳見此篇博客:JS中幾種常見的高階函數
二、作為返回值
判斷數據的類型
var Type = {};
for (var i = 0, type; type = [‘String‘, ‘Array‘, ‘Number‘ ][i++];) {
(function(type) {
Type[‘is‘ + type] = function(obj) {
return Object.prototype.toString.call(obj) === ‘[object ‘+ type +‘]‘;
}
})(type)
};
Type.isArray([]); // 輸出:true
Type.isString("str"); // 輸出:true
單例模式
var getSingle = function(fn) {
var ret;
return function() {
return ret || (ret = fn.apply(this, arguments));
};
};
三、實現AOP
AOP(面向切面編程)的主要作用是:把一些跟核心業務邏輯模塊無關的功能抽離出來,這些跟業務邏輯無關的功能通常包括日誌統計、安全控制、異常處理等。把這些功能抽離出來之後,再通過“動態植入”的方式摻入業務邏輯模塊中。
這樣做的好處首先是可以保持業務邏輯模塊的純凈和高內聚性,其次是可以很方便地復用日誌統計等功能模塊。
通常,在JavaScript中實現AOP,都是指把一個函數“動態植入”到另外一個函數之中,具體的實現技術有很多,下面的例子通過擴展Function.prototype來做到這一點。
Function.prototype.before = function(beforefn) {
var __self = this; // 保存原函數的引用
return function() { // 返回包含了原函數和新函數的"代理"函數
beforefn.apply(this, arguments); // 執行新函數,修正this
return __self.apply(this, arguments); // 執行原函數
}
};
Function.prototype.after = function(afterfn) {
var __self = this;
return function() {
var ret = __self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
}
};
var func = function() {
console.log(2);
};
func = func.before(function() {
console.log(1);
}).after(function() {
console.log(3);
});
func();
// 按順序打印出1,2,3
四、currying
currying(函數柯裏化),又稱部分求值。一個currying的函數首先會接受一些參數,接受了這些參數之後,該函數並不會立即求值,而是繼續返回另外一個函數,剛才傳入的參數在函數形成的閉包中被保存起來。待到函數被真正需要求值的時候,之前傳入的所有參數都會被一次性用於求值。
// 通用currying函數,接受一個參數,即將要被currying的函數
var currying = function(fn) {
var args = [];
return function() {
if (arguments.length === 0) {
return fn.apply(this, args);
} else {
[].push.apply(args, arguments);
return arguments.callee;// arguments.callee,指向當前函數的引用
}
}
};
// 將被currying的函數
var cost = (function() {
var money = 0;
return function() {
for (var i = 0, l = arguments.length; i < l; i++) {
money += arguments[i];
}
return money;
}
})();
var cost = currying( cost ); // 轉化成currying函數
cost( 100 ); // 未真正求值
cost( 200 ); // 未真正求值
cost( 300 ); // 未真正求值
console.log (cost()); // 求值並輸出:600
五、uncurrying
在JavaScript中,當我們調用對象的某個方法時,其實不用去關心該對象原本是否被設計為擁有這個方法,這是動態類型語言的特點,也是常說的鴨子類型思想。
同理,一個對象也未必只能使用它自身的方法,那麽有什麽辦法可以讓對象去借用一個原本不屬於它的方法呢?
答案對於我們來說很簡單,call和apply都可以完成這個需求,因為用call和apply可以把任意對象當作this傳入某個方法,這樣一來,方法中用到this的地方就不再局限於原來規定的對象,而是加以泛化並得到更廣的適用性。
而uncurrying的目的是將泛化this的過程提取出來,將fn.call或者fn.apply抽象成通用的函數。
// uncurrying實現
Function.prototype.uncurrying = function() {
var self = this;
return function() {
return Function.prototype.call.apply(self, arguments);
}
};
// 將Array.prototype.push進行uncurrying,此時push函數的作用就跟Array.prototype.push一樣了,且不僅僅局限於只能操作array對象。
var push = Array.prototype.push.uncurrying();
var obj = {
"length": 1,
"0": 1
};
push(obj, 2);
console.log(obj); // 輸出:{0: 1, 1: 2, length: 2}
六、函數節流
當一個函數被頻繁調用時,如果會造成很大的性能問題的時候,這個時候可以考慮函數節流,降低函數被調用的頻率。
throttle函數的原理是,將即將被執行的函數用setTimeout延遲一段時間執行。如果該次延遲執行還沒有完成,則忽略接下來調用該函數的請求。
throttle函數接受2個參數,第一個參數為需要被延遲執行的函數,第二個參數為延遲執行的時間。
var throttle = function(fn, interval) {
var __self = fn, // 保存需要被延遲執行的函數引用
timer, // 定時器
firstTime = true; // 是否是第一次調用
return function() {
var args = arguments,
__me = this;
if (firstTime) { // 如果是第一次調用,不需延遲執行
__self.apply(__me, args);
return firstTime = false;
}
if (timer) { // 如果定時器還在,說明前一次延遲執行還沒有完成
return false;
}
timer = setTimeout(function() { // 延遲一段時間執行
clearTimeout(timer);
timer = null;
__self.apply(__me, args);
}, interval || 500 );
};
};
window.onresize = throttle(function() {
console.log(1);
}, 500 );
事件結束
對於某些可以頻繁觸發的事件,有時候我們希望在事件結束後進行一系列操作。這時我們可以利用高階函數做如下處理:
function debounce(fn, interval) {
var timer = null;
function delay() {
var target = this;
var args = arguments;
return setTimeout(function(){
fn.apply(target, args);
}, interval);
}
return function() {
if (timer) {//如果定時器還存在,就清除
clearTimeout(timer);
}
timer = delay.apply(this, arguments);
}
};
window.onresize = debounce(function(){
console.log(‘resize end‘);
}, 500);
七、分時函數
當一次的用戶操作會嚴重地影響頁面性能,如在短時間內往頁面中大量添加DOM節點顯然也會讓瀏覽器吃不消,我們看到的結果往往就是瀏覽器的卡頓甚至假死。
這個問題的解決方案之一是下面的timeChunk函數,timeChunk函數讓創建節點的工作分批進行,比如把1秒鐘創建1000個節點,改為每隔200毫秒創建8個節點。
timeChunk函數接受3個參數,第1個參數是創建節點時需要用到的數據,第2個參數是封裝了創建節點邏輯的函數,第3個參數表示每一批創建的節點數量。
var timeChunk = function(ary, fn, count) {
var t;
var start = function() {
for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ){
var obj = ary.shift();
fn( obj );
}
};
return function() {
t = setInterval(function() {
if (ary.length === 0) { // 如果全部節點都已經被創建好
return clearInterval(t);
}
start();
}, 200); // 分批執行的時間間隔,也可以用參數的形式傳入
};
};
八、惰性加載函數
在Web開發中,因為瀏覽器之間的實現差異,一些嗅探工作總是不可避免。比如我們需要一個在各個瀏覽器中能夠通用的事件綁定函數addEvent,常見的寫法如下:
方案一:
var addEvent = function(elem, type, handler) {
if (window.addEventListener) {
return elem.addEventListener(type, handler, false);
}
if (window.attachEvent) {
return elem.attachEvent(‘on‘ + type, handler);
}
};
缺點:當它每次被調用的時候都會執行裏面的if條件分支,雖然執行這些if分支的開銷不算大,但也許有一些方法可以讓程序避免這些重復的執行過程。
方案二:
var addEvent = (function() {
if (window.addEventListener) {
return function(elem, type, handler) {
elem.addEventListener(type, handler, false);
}
}
if (window.attachEvent) {
return function(elem, type, handler) {
elem.attachEvent(‘on‘ + type, handler);
}
}
})();
缺點:也許我們從頭到尾都沒有使用過addEvent函數,這樣看來,一開始的瀏覽器嗅探就是完全多余的操作,而且這也會稍稍延長頁面ready的時間。
方案三:
var addEvent = function(elem, type, handler) {
if (window.addEventListener) {
addEvent = function(elem, type, handler) {
elem.addEventListener(type, handler, false);
}
} else if (window.attachEvent) {
addEvent = function(elem, type, handler) {
elem.attachEvent(‘on‘ + type, handler);
}
}
addEvent(elem, type, handler);
};
此時addEvent依然被聲明為一個普通函數,在函數裏依然有一些分支判斷。但是在第一次進入條件分支之後,在函數內部會重寫這個函數,重寫之後的函數就是我們期望的addEvent函數,在下一次進入addEvent函數的時候,addEvent函數裏不再存在條件分支語句。
JavaScript高階函數的應用