- 原文地址:Higher Order Functions (Composing Software)(part 4)
- 原文作者:Eric Elliott
- 譯文出自:掘金翻譯計劃
- 譯者:reid3290
- 校對者:Aladdin-ADD、avocadowang
Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)(譯註:該圖是用 PS 將煙霧處理成方塊狀後得到的效果,參見 flickr。)
注意:這是“軟體編寫”系列文章的第四部分,該系列主要闡述如何在 JavaScript ES6+ 中從零開始學習函數語言程式設計和組合化軟體(compositional software)技術(譯註:關於軟體可組合性的概念,參見維基百科 Composability)。後續還有更多精彩內容,敬請期待!
< 上一篇 | << 第一篇 | 下一篇 >
高階函式是一種接收一個函式作為輸入或輸出一個函式的函式(譯註:參見維基百科高階函式),這是和一階函式截然不同的。
之前我們看到的 .map()
和 .filter()
都是高階函式 —— 它們都接受一個函式作為引數,
先來看個一階函式的例子,該函式會將單詞陣列中 4 個字母的單詞過濾掉:
const censor = words => {
const filtered = [];
for (let i = 0, { length } = words; i < length; i++) {
const word = words[i];
if (word.length !== 4) filtered.push(word);
}
return filtered;
};
censor(['oops', 'gasp', 'shout', 'sun']);
// [ 'shout', 'sun' ]複製程式碼
如果又要選擇出所有以 's' 開頭的單詞呢?可以再定義一個函式:
const startsWithS = words => {
const filtered = [];
for (let i = 0, { length } = words; i < length; i++) {
const word = words[i];
if (word.startsWith('s')) filtered.push(word);
}
return filtered;
};
startsWithS(['oops', 'gasp', 'shout', 'sun']);
// [ 'shout', 'sun' ]複製程式碼
顯然可以看出這裡面有很多重複的程式碼,這兩個函式的主體是相同的 —— 都是遍歷一個陣列並根據給定的條件進行過濾。這便形成了一種特定的模式,可以從中抽象出更為通用的解決方案。
不難看出, “遍歷”和“過濾”都是亟待抽象出來的,以便分享和複用到其他所有類似的函式中去。畢竟,從陣列中選取某些特定元素是很常見的需求。
幸運的是,函式是 JavaScript 中的一等公民,就像數字、字串和物件一樣,函式可以:
- 像變數一樣賦值給其他變數
- 作為物件的屬性值
- 作為引數進行傳遞
- 作為函式的返回值
函式基本上可以像其他任何資料型別一樣被使用,這點使得“抽象”容易了許多。例如,可以定義一種函式,將遍歷陣列並累計出一個返回值的過程抽象出來,該函式接收一個函式作為引數來決定具體的累計過程,不妨將此函式稱為 reducer:
const reduce = (reducer, initial, arr) => {
// 共享的
let acc = initial;
for (let i = 0, length = arr.length; i < length; i++) {
// 獨特的
acc = reducer(acc, arr[i]);
// 又是共享的
}
return acc;
};
reduce((acc, curr) => acc + curr, 0, [1,2,3]); // 6複製程式碼
該 reduce()
接受 3 個引數:一個 reducer 函式、一個累計的初始值和一個用於遍歷的陣列。對陣列中的每個元素都會呼叫 reducer,傳入累計器和當前陣列元素,返回值又會賦給累計器。對陣列中的所有元素都執行過 reducer 之後,返回最終的累計結果。
在用例中,呼叫 reduce
並傳給它 3 個引數:reducer
函式、初始值 0 以及需要遍歷的陣列。其中 reducer
函式以累計器和當前陣列元素為引數,返回累計後的結果。
如此將遍歷和累計的過程抽象出來之後,便可實現更為通用的 filter()
函式:
const filter = (
fn, arr
) => reduce((acc, curr) => fn(curr) ?
acc.concat([curr]) :
acc, [], arr
);複製程式碼
在此 filter()
函式中,除了以引數形式傳進來的 fn()
函式以外,所有程式碼都是可複用的。其中 fn()
引數被稱為斷言(predicate) —— 返回一個布林值的函式。
將當前值傳給 fn()
,如果 fn(curr)
返回 true
,則將 curr
新增到結果陣列中並返回之;否則,直接返回當前陣列。
現在便可藉助 filter()
函式來實現過濾 4 字母單詞的 censor()
函式:
const censor = words => filter(
word => word.length !== 4,
words
);複製程式碼
喔!將所有公共程式碼抽象出來之後,censor()
函式便十分簡潔了。
startsWithS()
也是如此:
const startsWithS = words => filter(
word => word.startsWith('s'),
words
);複製程式碼
你若稍加留意便會發現 JavaScript 其實已經為我們做了這些抽象,即 Array.prototype
的相關方法,例如 .reduce()
、.filter()
、.map()
等等。
高階函式也常常被用於對不同資料型別的操作進行抽象。例如,.filter()
函式不一定非得作用於字串陣列。只需傳入一個能夠處理不同資料型別的函式,.filter()
便能過濾數字了。還記得 highpass
的例子嗎?
const highpass = cutoff => n => n >= cutoff;
const gt3 = highpass(3);
[1, 2, 3, 4].filter(gt3); // [3, 4];複製程式碼
換言之,高階函式可以用來實現函式的多型性。如你所見,相對於一階函式而言,高階函式的複用性和通用性更好。一般來講,在實際編碼中會組合使用高階函式和一些非常簡單的一階函式。
接下來
想學習更多 JavaScript 函數語言程式設計嗎?
跟著 Eric Elliott 學 Javacript,機不可失時不再來!
Eric Elliott 是 “編寫 JavaScript 應用” (O’Reilly) 以及 “跟著 Eric Elliott 學 Javascript” 兩書的作者。他為許多公司和組織作過貢獻,例如 Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN 和 BBC等 , 也是很多機構的頂級藝術家,包括但不限於 Usher、Frank Ocean 以及 Metallica。
大多數時間,他都在 San Francisco Bay Area,同這世上最美麗的女子在一起。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃。