注:文章最末尾有個人公眾號二維碼,會分享更多技術文章等,敬請關注
本文講解的高階函式是之前講解的閉包的續集,所以在學習高階函式之前,一定要確保對閉包以及作用域的概念已經有了解:
理解抽象
引出抽象的概念
有Java、C#等開發經驗的同學對程式碼抽象的思想一定不會陌生,抽象類、介面平時寫的非常多,但是對於一直都從事前端開發的同學來說,“抽象”這個詞就比較陌生了,畢竟JavaScript中沒有abstract、interface。
但是JS中肯定是有程式碼抽象的思想的,只不過是形式上和Java等語言不同罷了!
先來看Java中的一個抽象類:
public abstract class SuperClass {
public abstract void doSomething();
}
複製程式碼
這是Java中的一個類,類裡面有一個抽象方法doSomething,現在不知道子類中要doSomething方法做什麼,所以將該方法定義為抽象方法,具體的邏輯讓子類自己去實現。
建立子類去實現SuperClass:
public class SubClass extends SuperClass{
public void doSomething() {
System.out.println("say hello");
}
}
複製程式碼
SubClass中的doSomething輸出字串“say hello”,其他的子類會有其他的實現,這就是Java中的抽象類與實現。
那麼JS中的抽象是怎麼樣的,最為經典的就是回撥函式了:
function createDiv(callback) {
let div = document.createElement('div');
document.body.appendChild(div);
if (typeof callback === 'function') {
callback(div);
}
}
createDiv(function (div) {
div.style.color = 'red';
})
複製程式碼
這個例子中,有一個createDiv這個函式,這個函式負責建立一個div並新增到頁面中,但是之後要再怎麼操作這個div,createDiv這個函式就不知道,所以把許可權交給呼叫createDiv函式的人,讓呼叫者決定接下來的操作,就通過回撥的方式將div給呼叫者。
這也是體現出了抽象,既然不知道div接下來的操作,那麼就直接給呼叫者,讓呼叫者去實現。 和Java中抽象類中的抽象方法的思想是一樣的。
總結一下抽象的概念:抽象就是隱藏更具體的實現細節,從更高的層次看待我們要解決的問題。
陣列中的遍歷抽象
在程式設計的時候,並不是所有功能都是現成的,比如上面例子中,可以建立好幾個div,對每個div的處理都可能不一樣,需要對未知的操作做抽象,預留操作的入口,作為一名程式設計師,我們需要具備這種在恰當的時候將程式碼抽象的思想。
接下來看一下ES5中提供的幾個陣列操作方法,可以更深入的理解抽象的思想,ES5之前遍歷陣列的方式是:
var arr = [1, 2, 3, 4, 5];
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
console.log(item);
}
複製程式碼
仔細看一下,這段程式碼中用for,然後按順序取值,有沒有覺得如此操作有些不夠優雅,為出現錯誤留下了隱患,比如把length寫錯了,一不小心複用了i。既然這樣,能不能抽取一個函式出來呢?最重要的一點,我們要的只是陣列中的每一個值,然後操作這個值,那麼就可以把遍歷的過程隱藏起來:
function forEach(arr, callback) {
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
callback(item);
}
}
forEach(arr, function (item) {
console.log(item);
});
複製程式碼
以上的forEach方法就將遍歷的細節隱藏起來的了,把使用者想要操作的item返回出來,在callback還可以將i、arr本身返回:callback(item, i, arr)
。
JS原生提供的forEach方法就是這樣的:
arr.forEach(function (item) {
console.log(item);
});
複製程式碼
跟forEach同族的方法還有map、some、every等。思想都是一樣的,通過這種抽象的方式可以讓使用者更方便,同事又讓程式碼變得更加清晰。
抽象是一種很重要的思想,讓可以讓程式碼變得更加優雅,並且操作起來更方便。在高階函式中也是使用了抽象的思想,所以學習高階函式得先了解抽象的思想。
高階函式
什麼是高階函式
至少滿足以下條件的中的一個,就是高階函式:
-
將其他函式作為引數傳遞
-
將函式作為返回值
簡單來說,就是一個函式可以操作其他函式,將其他函式作為引數或將函式作為返回值。我相信,寫過JS程式碼的同學對這個概念都是很容易理解的,因為在JS中函式就是一個普通的值,可以被傳遞,可以被返回。
引數可以被傳遞,可以被返回,對Java等語言開發的同學理解起來可能會稍微麻煩一些,因為Java語言沒有那麼的靈活,不過Java8的lambda大概就是這意思;
函式作為引數傳遞
函式作為引數傳遞就是我們上面提到的回撥函式,回撥函式在非同步請求中用的非常多,使用者想要在請求成功後利用請求回來的資料做一些操作,但是又不知道請求什麼時候結束。
用jQuery來發一個Ajax請求:
function getDetailData(id, callback) {
$.ajax('http://xxxxyyy.com/getDetailData?' + id, function (res) {
if (typeof callback === 'function') {
callback(res);
}
});
}
getDetailData('78667', function (res) {
// do some thing
});
複製程式碼
類似Ajax這種操作非常適合用回撥去做,當一個函式裡不適合執行一些具體的操作,或者說不知道要怎麼操作時,可以將相應的資料傳遞給另一個函式,讓另一個函式來執行,而這個函式就是傳遞進來的回撥函式。
另一個典型的例子就是陣列排序。
函式作為值返回
在判斷資料型別的時候最常用的是typeof,但是typeof有一定的侷限性,比如:
console.log(typeof []); // 輸出object
console.log(typeof {}); // 輸出object
複製程式碼
判斷陣列和物件都是輸出object,如果想要更細緻的判斷應該要使用Object.prototype.toString
console.log(Object.prototype.toString.call([])); // 輸出[object Array]
console.log(Object.prototype.toString.call({})); // 輸出[object Object]
複製程式碼
基於此,我們可以寫出判斷物件、陣列、數字的方法:
function isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
function isNumber(number) {
return Object.prototype.toString.call(number) === '[object Number]';
}
複製程式碼
我們發現這三個方法太像了,可以做一些抽取:
function isType(type) {
return function (obj) {
return Object.prototype.toString.call(obj) === '[object ' + type + ']';
}
}
var isArray = isType('Array');
console.log(isArray([1,2]));
複製程式碼
這個isType方法就是高階函式,該函式返回了一個函式,並且利用閉包,將程式碼變得優雅。
高階函式的應用
lodash中的使用
高階函式在平時的開發中用的非常多,只是有時候你不知道你的這種用法就是高階函式,在一些開源的類庫中也用的很多,比如很有名的 lodash,挑其中一個before函式:
function before(n, func) {
let result
if (typeof func != 'function') {
throw new TypeError('Expected a function')
}
return function(...args) {
if (--n > 0) {
result = func.apply(this, args)
}
if (n <= 1) {
func = undefined
}
return result
}
}
複製程式碼
在before函式中,同時有用到將函式當做傳遞進來,又返回了一個函式,這是一個很經典的高階函式的例子。
看一下該程式碼可以怎麼用吧:
jQuery(element).on('click', before(5, addContactToList))
複製程式碼
所以before函式就是讓某個方法最多呼叫n次。
注:before函式程式碼不難,使用也不難,但就是這麼一個簡單的工具方法需要了解的知識點有:作用域、閉包、高階函式,所以說知識點都是連貫的,接下來要寫的JavaScript設計模式系列,同樣也要用到這些知識。
函式節流
在寫程式碼的時候,大多數情況都是由我們自己主動去呼叫函式。不過在有一些情況下,函式的呼叫不是由使用者直接控制的,在這種情況下,函式有可能被廢除頻繁的呼叫,從而造成效能問題。
在 Element-UI 中,有一個 el-autocomplete 元件,該元件可以在使用者輸入的時候在輸入框下方列出相關輸入項:
其實就是可以在使用者輸入的時候,可以用已經輸入的內容做搜尋,餓了麼在實現該元件的時候是利用input元件,並且監聽使用者的輸入:
用input事件去監聽使用者輸入的話,使用者輸入的每一個字都會觸發該方法,如果是要用輸入的內容去做網路搜尋,使用者輸入的每一字都搜尋的話,觸發的頻率太高了,效能消耗就有點大了,而且在網路比較差的情況下使用者體驗也比較不好。
餓了麼實現該元件的時候當然也考慮到了這些問題,用的是業界比較通用的做法→節流,就是當輸入後,延遲一段時間再去執行搜尋,如果該次延遲執行還沒有完成的話,就忽略接下來搜尋的請求。
看一下其實現:
autocomplete的節流思想就是剛才說的那種,並且用了 throttle-debounce 這個工具庫,其實現就是利用高階函式,有興趣的同學可以看它的原始碼:https://github.com/niksy/throttle-debounce,程式碼並不複雜。
高階函式還有其他的用法,比如用在設計模式中等,這些內容將會在後面詳細介紹。
特別注意
可以關注我的公眾號:icemanFE,接下來持續更新技術文章!