定義
高階函式是指至少滿足下列條件之一的函式:
- 函式可以作為引數被傳遞;
- 函式可以作為返回值輸出。
JavaScript語言中的函式顯然滿足高階函式的條件,在實際開發中,無論是將函式當作引數傳遞,還是讓函式的執行結果返回另外一個函式,這兩種情形都有很多應用場景,以下就是一些高階函式的應用。
應用
作為引數傳遞
ajax非同步請求
1 2 3 4 5 6 7 8 9 10 11 12 |
// 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 2 3 4 5 6 7 8 9 10 11 |
//從小到大排列 [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] |
作為返回值
判斷資料的型別
1 2 3 4 5 6 7 8 9 10 11 12 |
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 |
單例模式
1 2 3 4 5 6 |
var getSingle = function(fn) { var ret; return function() { return ret || (ret = fn.apply(this, arguments)); }; }; |
實現AOP
AOP(面向切面程式設計)的主要作用是把一些跟核心業務邏輯模組無關的功能抽離出來,這些跟業務邏輯無關的功能通常包括日誌統計、安全控制、異常處理等。把這些功能抽離出來之後,再通過“動態織入”的方式摻入業務邏輯模組中。這樣做的好處首先是可以保持業務邏輯模組的純淨和高內聚性,其次是可以很方便地複用日誌統計等功能模組。
通常,在JavaScript中實現AOP,都是指把一個函式“動態織入”到另外一個函式之中,具體的實現技術有很多,下面的例子通過擴充套件Function.prototype來做到這一點。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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的函式首先會接受一些引數,接受了這些引數之後,該函式並不會立即求值,而是繼續返回另外一個函式,剛才傳入的引數在函式形成的閉包中被儲存起來。待到函式被真正需要求值的時候,之前傳入的所有引數都會被一次性用於求值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// 通用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; } } }; // 將被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抽象成通用的函式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 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個引數,第一個引數為需要被延遲執行的函式,第二個引數為延遲執行的時間。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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 ); |
分時函式
當一次的使用者操作會嚴重地影響頁面效能,如在短時間內往頁面中大量新增DOM節點顯然也會讓瀏覽器吃不消,我們看到的結果往往就是瀏覽器的卡頓甚至假死。
這個問題的解決方案之一是下面的timeChunk函式,timeChunk函式讓建立節點的工作分批進行,比如把1秒鐘建立1000個節點,改為每隔200毫秒建立8個節點。
timeChunk函式接受3個引數,第1個引數是建立節點時需要用到的資料,第2個引數是封裝了建立節點邏輯的函式,第3個參數列示每一批建立的節點數量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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,常見的寫法如下:
方案一:
1 2 3 4 5 6 7 8 9 |
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分支的開銷不算大,但也許有一些方法可以讓程式避免這些重複的執行過程。
方案二:
1 2 3 4 5 6 7 8 9 10 11 12 |
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的時間。
方案三:
1 2 3 4 5 6 7 8 9 10 11 12 |
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函式裡不再存在條件分支語句。