在通常的程式語言中,函數的參數只能是基本型別或物件引用,回傳值也只是基本資料型別或物件引用。但在Javascript中函數作為一等公民,既可以當做參數傳遞,也可以被當作回傳值傳回。所謂高階函數就是可以把函數當作參數,或是將函數當作傳回值的函數。這兩種情況在實際開發中有很多應用場景,本文是我在工作學習中遇到的幾個應用場景的總結。
回呼函數
程式碼重複使用是衡量一個應用程式的重要標準之一。透過將變化的業務邏輯抽離封裝在回呼函數中能夠有效的提高程式碼復用率。例如ES5中為數組增加的forEach方法,遍歷數組,對每個元素呼叫同一個函數。
array = {}; array.forEach = function(arr, fn){ for (var i = 0, len = arr.length; i < len; i++) { fn(arr[i], i, arr); } }
透過回呼函數將業務的重點聚焦在回呼函數中,而不必每次都要重複編寫遍歷程式碼。
偏函數
作為將函數當做回傳值輸出的典型應用就是偏函數。所謂偏函數是指建立一個呼叫另一個部分——參數或變數已經預置的函數——的函數的用法。反正看著定義我是沒理解這東東幹嘛的。咱們還是先看例子吧,偏函數最典型的例子就是型別判斷。
Javascript物件都擁有三個屬性:原型屬性、類別屬性、可擴充性。 (不知道的同學要回去翻犀牛書哦,page:138)類別屬性是一個字串,Javascript中並未直接提供,但我們可以利用Object.prototype.toString來間接得到。函數總是傳回如下形式:
[object Class]
因此我們可以編寫一系列isType函數。
程式碼如下:
isString = function(obj){ return Object.prototype.toString.call(obj) === "[object String]"; } isNumber = function(obj){ return Object.prototype.toString.call(obj) === "[object Number]"; } isArray = function(obj){ return Object.prototype.toString.call(obj) === "[object Array]"; }
這幾個函數中大部分程式碼是重複的,這時高階函數便華麗麗的登場了:
isType = function(type) { return function(obj) { return Object.prototype.toString.call(obj) === "[object " + type + "]"; } } isString = isType('String'); isNumber = isType('Number'); isArray = isType('Array');
所以透過指定部分參數來傳回一個新的客製化函數的形式就是偏函數。
currying(柯里化)
currying又稱部分求值。一個currying的函數首先會接受一些參數,接受這些參數之後,函數並不會立即求值,而是繼續傳回另一個函數,剛才傳入的參數在函數形成的閉包中被保存。待到函數被真正需要求值的時候,之前傳入的所有參數都會被一次用來求值。
var currying = function(fn) { var args = []; return function() { if (arguments.length === 0) { return fn.applay(this, args); } else { args = args.concat(arguments); return arguments.callee; } } }
假設我們以計算一個月每天花銷為例:
var currying = function(fn) { debugger; var args = []; return function() { if (arguments.length === 0) { return fn.apply(this, args); } else { Array.prototype.push.apply(args, arguments); return arguments.callee; } } } cost = function(){ var sum = 0; for (var i = 0, len = arguments.length; i < len; i++) { sum += arguments[i]; } return sum; } var cost = currying(cost); cost(100); cost(200); alert(cost())
事件節流
在某些場景下,某些事件可能會被重複的觸發,但事件處理函數並不需要每次都執行。例如在window.resize事件中進行複雜的邏輯計算,如果使用者頻繁的改變瀏覽器大小,複雜計算會對效能造成嚴重影響;有時這些邏輯計算並不需要每次rezise時都觸發,只需要計算有限的幾次便可以。這時我們需要根據時間段來忽略一些事件請求。請看以下節流函數:
function throttle(fn, interval) { var doing = false; return function() { if (doing) { return; } doing = true; fn.apply(this, arguments); setTimeout(function() { doing = false; }, interval); } } window.onresize = throttle(function(){ console.log('execute'); }, 500);
透過控制函數執行時間,可以在函數執行次數與功能需求之間達到完美平衡。另一個事件是mousemove。如果我們給一個dom元素綁定該事件,滑鼠在改元素上移動時,該事件就會重複觸發。
事件結束
對於某些可以頻繁觸發的事件,有時我們希望在事件結束後進行一系列操作。這時我們可以用高階函數做以下處理:
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 = throttle(function(){ console.log('resize end'); }, 500);
如果在這過程中事件被觸發則清除上一次事件句柄,重新綁定執行時間。
參考資料:
《深入淺出node》
《Javascript設計模式與開發實務》