一、柯里化 - currying
核心代碼實現:
export function curry(fn) { let len = fn.length; // 收集目標函數的參數個數(總) return function curriedFn(...params) { // 說明當前收集到的參數還沒有達到目標函數需要接收到的,需要返回一個新的函數繼續去收集 if (params.length < len) { return function () { // 一個關鍵是下面這個 arguments 是屬於當前這個匿名函數的 // 另外一個關鍵是 params.concat(...), 這裏其實應用了尾調用優化 return curriedFn.apply( null, params.concat([].slice.call(arguments)) ) } } return fn.apply(null, params); } }
let curriedSum = curry(sum); // curriedSum 是一個函數 let tmpA = curriedSum(1); // tmpA 是一個函數 let tmpB = tmpA(2); // tmpB 是一個函數 let tmpC = tmpB(3); // tmpC 則是最終的運算結果,就是原函數 sum(1, 2, 3) 的運算結果,即不是一個函數
- 執行第一句, curry 函數執行並形成閉包(暫時命名成 curry-[fn] ),len 記錄爲 3,返回 curriedFn ,即 curriedSum 就是 curriedFn
- 執行第二句,其實執行的是 curriedFn(1) ,此時 params = [1] ,進入邏輯判斷, curriedFn 形成一層閉包(暫時命名成 curriedFn-[1] ),返回匿名函數,即 tmpA 就是這個匿名函數
- 執行第三句,運行這個匿名函數,用僞代碼表示如下,這句有兩個關鍵點:
let tmpA = function () { return curriedFn.call(null, params.concat([].slice.call(arguments))) } tmpA(2);
- 首先是 arguments,這個變量屬於這個匿名函數,而在此時變量的值就是 [2] (傳入的變量就是 2,如果是 tmpA(2, 3) 這種,那麼 arguments = [2, 3] )
- 其次 params.concat([].slice.call(arguments)) ,這個結合緩存的結果就是 [1].concat([2]) ,即 [1, 2] ,然後將這個值作爲參數進行遞歸 curriedFn([1, 2]) ,所以說這裏其實應用了尾遞歸優化
所以經過運算,最終 tmpB 也是那個匿名函數,而第二句中不是形成了一個 curriedFn-[1] 的閉包嘛,此時也被釋放,而隨着 curriedFn([1, 2]) 的執行,形成一個新的閉包(暫時命名成 curriedFn-[1, 2] ),在這個新的閉包中, params = [1, 2]
- 執行第四句,走的是 curriedFn 中 return fn.apply(null, params); 這句,得出最終的運算結果,這裏就不詳敘述
其他
組合指的是將多個函數組合成一個函數,這樣一個函數的輸出就可以作爲另一個函數的輸入,從而實現多個函數的鏈式調用
組合compose可以提高代碼的可讀性和可維護性,減少重複代碼的出現,更加便捷的實現函數的複用
用表達式表示: compose(f, g, t) => x => f(g(t(x))) ,進一步結合柯里化則是 compose(f)(g)(t) => x => f(g(t(x)))
概念上, compose 函數像是 curry 的逆運算,把多個函數鏈接起來依次調用,不用過於關注其中的執行過程,直接得到最終結果,這樣最直觀的好處是節省了一堆臨時變量核心代碼實現:
// 普通版 export const compose = (...fns) => (...args) => fns.reduceRight((val, fn) => fn.apply(null, [].concat(val)), args); // 異步版 export const compose = (...fns) => (input) => fns.reduceRight((chain, fn) => chain.then(fn), Promise.resolve(input)); // 普通版支持鏈接後多參數入參,而異步版則只支持單參數(更符合函數式編程思想)
三、管道 - pipe
管道其實是組合的另外一個版本,組合是從右向左依次調用處理函數,使用的是 reduceRight ,而管道則是從左向右依次調用處理函數,使用的是 reduce 函數
四、實踐經驗
一、柯里化中把要操作的數據放到最後
// 推薦 const target = (x, str) => str.split(x); // 不推薦 const target = (str, x) => str.split(x);
二、函數組合中函數要求單輸入
即:傳給 compose 函數的參數,最好是經過 curry 化的函數
三、函數組合的 Debug
// debugger 函數,其中 x 是 reverse 這個函數的計算值 const trace = curry((tip, x) => { console.log(tip, x); return x; }); const fn = compose(toUpperCase, head, trace('after reverse'), reverse);