JS高階函數精講

2020-07-16 10:05:04
高階函數也稱運算元(運算子)或泛函。作為函數語言程式設計最顯著的特徵,高階函數是對函數運算進行進一步的抽象。高階函數的形式應至少滿足下列條件之一:
  • 函數可以作為函數被傳入,也稱為回撥函數,如函數合成運算。
  • 可以返回函數作為輸出,如函數柯里化運算。

JS回撥函數

把函數作為值傳入另一個引數,當傳入引數被呼叫時,就稱為回撥函數,即非同步呼叫已繫結的函數。例如,事件處理常式、定時器中的回撥函數、非同步請求中的回撥函數、replace 方法中的替換函數、陣列疊代中的回撥函數(sort、map、forEach、filter、some、every、reduce 和 reduceRight 等),都是回撥函數的不同應用形式。下面僅舉兩個範例,演示回撥函數的應用。

範例1

下面程式碼根據日期對物件進行排序。
//宣告3個物件,每個物件都有屬性id和date
var a = {id : 1, date : new Date(2019,3,12)},
    b = {id : 2, date : new Date(2019,1,14)},
    c = {id : 3, date : new Date(2019,2,26)};
var arr = [a,b,c];
arr.sort(function(x,y){
    return x.date-y.date;
});
for (var i = 0; i < arr.length; i++) {
console.log(arr[i].id + " " + arr[i].date.toLocaleString());
}
輸出結果:

2 2019 年 2 月 14 日 0:00:00
3 2019 年 3 月 26 日 0:00:00
1 2019 年 4 月 12 日 0:00:00

在陣列排序的時候,會疊代陣列每個元素,並逐一呼叫回撥函數 function(x,y) {return x.date - y.date}。

範例2

在《JS map()》一節中我們曾介紹過陣列的 map 方法,實際上很多函數語言程式設計語言均有此函數。其語法格式為:

map(array,func)

map 表示式將 func 函數作用於 array 的每一個元素,並返回一個新的 array。

下面使用 JavaScript 實現 map(array,func) 表示式運算。
function map(array,func) {
    var res = [];
    for (var i in array) {
        res.push(func(array[i]));
    }
    return res;
}
console.log(map([1,3,5,7,8], function (n) {  //返回元素值的平方
    return n * n;
}));  //1,9,25,49,64
console.log(map(["one", "two", "three", "four"], function(item) {  //返回首字母大寫
    return item[0].toUpperCase() + item.slice(1).toLowerCase();
}));  //One,Two,Three,Four
兩次呼叫 map,卻得到了截然不同的結果,是因為 map 的引數本身已經進行了一次抽象,map 函數做的是第二次抽象。注意:高階的“階”可以理解為抽象的層次。

JS 函數既可以作為引數傳入函數內部,也可以作為返回值 return 到函數外部,具體應用場景包括:
  • JS 單例模式
  • JS 實現 AOP
  • JS 型別檢測
  • JS 函數節流和分時函數
  • JS 惰性載入函數與分支函數
  • JS 偏函數
  • JS 泛型函數

由於篇幅有限,本節只介紹前面三種應用場景,其它場景請猛擊連結檢視。

JS單例模式

單例就是保證一個類只有一個範例。實現方法:先判斷範例是否存在,如果存在則直接返回,否則就建立範例再返回。

單例模式可以確保一個型別只有一個範例物件。在 JavaScript 中,單例可以作為一個名稱空間,提供一個唯一的存取點來存取該物件。單例模式封裝程式碼如下:
var getSingle = function (fn) {
    var ret;
    return function () {
        return ret || (ret = fn.apply(this, arguments));
    };
};

範例1

在指令碼中定義 XMLHttpRequest 物件。由於一個頁面可能需要多次建立非同步請求物件,使用單例模式封裝之後,就不用重複建立範例物件,共用一個即可。
function XHR () {  //定義XMLHttpRequest 物件
    return new XMLHttpRequest();
}
var xhr = getSingle(XHR);  //封裝XHR範例
var a = xhr();  //範例1
var b = xhr();  //範例2
console.log(a === b);  //true,說明這兩個範例實際上相同

範例2

可以限定函數僅能呼叫一次,避免重複呼叫,這在事件處理常式中非常有用。
<button>僅能點選一次</button>
<script>
function getSingle (fn) {
    var ret;
    return function () {
        return ret || (ret = fn.apply(this,arguments));
    };
};
var f = function () { console.log(this.nodeName); }  //事件處理常式
document.getElementsByTagName("button")[0].onclick = getSingle(f);
</script>

JS實現 AOP

AOP(面向切面程式設計)就是把一些與業務邏輯模組無關的功能抽離出來,如紀錄檔統計、安全控制、例外處理等,然後通過“動態織入”的方式摻入業務邏輯模組中。這樣設計的好處是:首先可以保證業務邏輯模組的純淨和高內聚性;其次可以方便地複用紀錄檔統計等功能模組。

範例

在 JavaScript 中實現 AOP,一般是把一個函數“動態織入”到另外一個函數中。具體的實現方法有很多,下面通過擴充套件 Function.prototype 方法實現 AOP。
Function.prototype.before = function (beforefn) {
    var __self = this;  //儲存原函數的參照
    return function () {  //返回包含了原函數和新函數的“代理”函數
        beforefn.apply(this, arguments);  //執行新函數
        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);  //執行新函數,修正this
        return ret;
    }
};
var func = function (){
    console.log(2);
};
func = func.before(function () {
    console.log(1);
}).after(function () {
    console.log(3)
});
func();  //按順序輸出1,2,3

型別檢測

本節利用 JavaScript 高階函數特性來重新設計 typeOf() 函數,並提供單項型別判斷函數。

【實現程式碼】

function typeOf(obj) {  //型別檢測函數,返回字串表示
    var str = Object.prototype.toString.call(obj);
    return str.match(/[object(.*?)]/)[1].toLowerCase();
};
['null', 'Undefined', 'Object', 'Array', 'String', 'Number', 'Boolean', 'Function', 'RegExp'].forEach(function (t) {  //型別判斷,返回布林值
    typeOf['is' + t] = function (o) {
        return typeOf(o) === t.toLowerCase();
    };
});

【應用程式碼】

//型別檢測
console.log(typeOf({}));  //"object"
console.log(typeOf([]));  //"array"
console.log(typeOf(0));  //"number"
console.log(typeOf(null));  //"null"
console.log(typeOf(undefined));  //"undefined"
console.log(typeOf(//));  //"regex"
console.log(typeOf(new Date()));  //"date"
//型別判斷
console.log(typeOf.isObject({}));  //true
console.log(typeOf.isNumber(NaN));  //true
console.log(typeOf.isRegExp(true));  //false