高階函式(軟體編寫)(第四部分)

檻外畸人發表於2019-02-23

高階函式(軟體編寫)(第四部分)

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];複製程式碼

換言之,高階函式可以用來實現函式的多型性。如你所見,相對於一階函式而言,高階函式的複用性和通用性更好。一般來講,在實際編碼中會組合使用高階函式和一些非常簡單的一階函式。

再續 “Reduce” >

接下來

想學習更多 JavaScript 函數語言程式設計嗎?

跟著 Eric Elliott 學 Javacript,機不可失時不再來!

高階函式(軟體編寫)(第四部分)

Eric Elliott“編寫 JavaScript 應用” (O’Reilly) 以及 “跟著 Eric Elliott 學 Javascript” 兩書的作者。他為許多公司和組織作過貢獻,例如 Adobe SystemsZumba FitnessThe Wall Street JournalESPNBBC等 , 也是很多機構的頂級藝術家,包括但不限於 UsherFrank Ocean 以及 Metallica

大多數時間,他都在 San Francisco Bay Area,同這世上最美麗的女子在一起。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章