JavaScript ES6函數式編程(二):柯里化、偏應用和組合、管道

  • 2019 年 10 月 22 日
  • 筆記

上一篇介紹了閉包和高階函數,這是函數式編程的基礎核心。這一篇來看看高階函數的實戰場景。

首先強調兩點:

  • 注意閉包的生成位置,清楚作用域鏈,知道閉包生成後快取了哪些變數
  • 高階函數思想:以變數作用域作為根基,以閉包為工具來實現各種功能

柯里化(curry)

定義:柯里化是把一個多參數函數轉換為一個嵌套的一元函數的過程

先看個簡單的例子,這是一個名為 add 的函數:const add = (x, y) => x + y;調用該函數 add(1, 1)、add(1, 2)、add(1, 3)…很普通,缺乏靈活性。

下面是柯里化實現版本:

const addCurried = x => y => x + y;

如果我們用一個單一的參數調用 addCurried,const add1 = addCurried(1)它返回一個函數fn = y => 1 + y,在其中 x 值通過閉包快取下來。接下來,我們繼續傳參add1(1); add1(2); add1(3),有沒有感覺比上面的 add 靈活。

上面的實現只是針對接收兩個參數相加的柯里化函數,接下來正是開始實現個基礎的通用的接收兩個參數的柯里化函數:

const curry = (binaryFn) => {      return function (firstArg) {          return function (secondArg) {              return binaryFn (firstArg, secondArg) ;  // 為啥要嵌套那麼多呢?基於什麼思路呢?思考一下...          };      };  };

現在可以用如下方式通過 curry 函數把 add 函數轉換成一個柯里化版本:

const autoCurriedAdd = curry(add)  autoCurriedAdd(1)(1)  // 2

這裡我們已經體會到柯里化的好處了,那麼柯里化是怎樣實現的呢?看上面 curry 的實現很容易發現,先傳入一個接受二元函數,然後返回一個一元函數,當這個一元函數執行後,再返回一個一元函數,再次執行返回的一元函數時,觸發最開始那個二元函數的執行。

這裡有一個點很重要——執行時機,接收夠兩個參數(add 函數接收的參數數量)立即執行,也就是說接收夠被柯里化函數的參數數量時觸發執行

好的,我們已經實現了一個基礎的柯里化函數。不過,這個 柯里化函數有很大的局限性——只能用於接收兩個參數的函數。我們需要的是被柯里化函數的參數可以任意數量,怎麼辦呢?還好我們已經知道了被柯里化函數的執行時機——接收夠被柯里化函數的參數數量時觸發執行。下面我們來實現更複雜的柯里化:

// 柯里化函數  const curry = (fn) => {    if (typeof fn !== 'function') {      throw Error('No function provided')    }      return function curriedFn (...args) {      if (fn.length > args.length) {  // 未達到觸發條件,繼續收集參數        return function () {          return curriedFn.apply(null, args.concat([].slice.call(arguments)))        }      }      return fn.apply(null, args)    }  }

這樣,我們就能處理多個參數的函數了。比如:

const multiply = (x, y, z) => x*y*z;    const curryMul = curry(multiply);  const result = curryMul(1)(2)(3); // 1*2*3 = 6

偏應用(partial)

偏應用,又稱作部分應用,它允許開發者部分地應用函數參數。實際上,偏應用是為一個多元函數預先提供部分參數,從而在調用時可以省略這些參數

比如我們要在每10ms做一組操作。可以通過 setTimeout 函數以如下方式實現:

setTimeout( () => console.log("Do X task"), 10);  setTimeout( () => console.log("Do Y tash"), 10);

很顯然,我們可以用上面的 curry 函數包裝成柯里化函數,實現靈活調用:

// 實現一個二元函數,用於柯里化  const setTimeoutWrapper = (time, fn) => {      setTimeout(fn, time);  }    // 使用 curry 函數封裝 setTimeout 來實現一個10ms延遲  const delayTenMs = curry(setTimeoutWrapper)  delayTenMs( () => console.log("Do X task") );  delayTenMs( () => console.log("Do Y task") );

很棒,也能實現靈活調用。但問題是我們不得不創建 setTimeoutWrapper 一樣的封裝器,這也是一種開銷。下面我們看看偏應用的實現:

// 偏應用函數  const partial = (fn, ...partialArgs) => {    let args = partialArgs    return (...fullArguments) => {      let count = 0      for (let i = 0; i < args.length && count < fullArguments; i++) {        if (args[i] === undefined) {          args[i] = fullArguments[count++]        }      }      return fn.apply(null, args)    }  }

下面用偏應用解決上面的延時10ms問題:

let delayTenMs = partial(setTimeout, undefined, 10);  // 注意此處,讓我們少創建了一個 setTimeoutWrapper 封裝器  delayTenMs( () => console.log("Do X task") )  delayTenMs( () => console.log("Do Y task") );

現在我們對柯里化有了更清晰的認識。創建偏應用函數時,第一個參數接收一個函數,剩餘參數是第一個傳入函數所需參數。剩餘參數待傳入的用undefined佔位,執行偏應用函數時填充undefined

組合(compose)

在了解什麼是函數式組合之前,讓我們理解組合的概念。

符合「|」被稱為管道,它允許我們通過組合一些函數去創建一個能夠解決問題的新函數。大致來說,「|」將最左側的函數輸出作為輸入發送給最右側的函數!從技術上講,該處理過程稱為「管道」。

compose 函數:

const compose = (a, b) => (c) => a(b(c))

compose 函數會首先執行 b 函數,並將 b 的返回值作為參數傳遞給 a。該函數調用的方向是從右至左的(先執行 b,再執行 a)。

可以看到,組合函數 compose 就是傳入一些函數。對於傳入的函數,我們要求一個函數只做一件事

下面看下如何應用 compose 函數:

// 通過組合計算字元串單詞個數  let splitIntoSpaces = (str) => str.split(" ");   // 分割成數組  let count = (array) => array.length;  // 計算長度    const countWords = compose(count, splitIntoSpaces);    countWord("hello your reading about composition"); // 5

上面的 compose 只能實現兩個函數的組合。如何組合更多個函數呢?這就需要藉助reduce的威力了:

// 組合多個函數 composeN  const composeN = (...fns) =>      (value) =>          fns.reverse().reduce((acc, fn) => fn(acc), value);

管道/序列(pipe)

管道和組合的概念很類似,都是串列處理數據。唯一區別就是執行方向:組合從右向左執行,管道從左向右執行。

// 組合多個函數 pipe  const pipe= (...fns) =>      (value) =>          fns.reduce((acc, fn) => fn(acc), value);

下面看下如何應用 pipe 函數:

// 通過管道計算字元串單詞個數  let splitIntoSpaces = (str) => str.split(" ");   // 分割成數組  let count = (array) => array.length;  // 計算長度    const countWords = pipe(splitIntoSpaces, count);  // 注意此處的傳參順序    countWord("hello your reading about composition"); // 5

總結

至此,我們學習了高階函數的一些應用——柯里化、偏應用、組合和管道,每種應用都有特定的應用場景。

其中,柯里化是最常用的一種場景,它的作用是把一個多參數函數轉換為一個嵌套的一元函數的過程。隨著閉包的產生,我們可以靈活的調用。

組合和管道類似,都是串列處理數據。傳入一個初始數據,通過一系列特定順序的純函數處理成我們希望得到的數據。