上一篇介紹了閉包和高階函數,這是函數式編程的基礎核心。這一篇來看看高階函數的實戰場景。html
首先強調兩點:編程
定義:柯里化是把一個多參數函數轉換爲一個嵌套的一元函數的過程。數組
先看個簡單的例子,這是一個名爲 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 靈活。app
上面的實現只是針對接收兩個參數相加的柯里化函數,接下來正是開始實現個基礎的通用的接收兩個參數的柯里化函數:ide
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
偏應用,又稱做部分應用,它容許開發者部分地應用函數參數。實際上,偏應用是爲一個多元函數預先提供部分參數,從而在調用時能夠省略這些參數。
好比咱們要在每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 函數:
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 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 函數式編程——入門篇