JavaScript ES6函數語言程式設計(二):柯里化、偏應用、組合、管道
上一篇介紹了閉包和高階函式,這是函數語言程式設計的基礎核心。這一篇來看看高階函式的實戰場景。
首先強調兩點:
- 注意閉包的生成位置,清楚作用域鏈,知道閉包生成後快取了哪些變數
- 高階函式思想:以變數作用域作為根基,以閉包為工具來實現各種功能
柯里化(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
總結
通過這一節的學習,我們知道了高階函式的一些應用——柯里化、偏應用、組合和管道,每種應用都有特定的應用場景。
其中,柯里化是最常用的一種場景,它的作用是把一個多引數函式轉換為一個巢狀的一元函式的過程。隨著閉包的產生,我們可以靈活的呼叫。
組合和管道類似,都是序列處理資料。傳入一個初始資料,通過一系列特定順序的純函式處理成我們希望得到的資料。
參考連結:
簡明 JavaScript 函數語言程式設計——入門