JS 中的函數式 - 常用概念

一、柯里化 - currying

柯里化的意思是將一個多元函數,轉換成一個依次調用的單元函數,用表達式表示: f(a,b,c) → f(a)(b)(c) 
 

核心代碼實現:

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);
  }
}

 

理解難點在於  curriedFn 中的邏輯判斷爲何需要返回一個匿名函數(以  anonymousFn  來指代)
 
比如 sum(1, 2, 3) 這種函數,經過柯里化後並調用的形式是 curry(sum)(1)(2)(3) 這樣:
當然這種形式可以分解成四句:
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);
    1. 首先是 arguments,這個變量屬於這個匿名函數,而在此時變量的值就是  [2] (傳入的變量就是 2,如果是 tmpA(2, 3)  這種,那麼 arguments = [2, 3] )
    2. 其次 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);  這句,得出最終的運算結果,這裏就不詳敘述

其他

當然,柯里化的含義比較嚴格,只有 f(a,b,c) => f(a)(b)(c)  這樣的形式才能叫真正的柯里化
而 f(a, b, c) => f(a)(b, c) 或者 f(a, b, c) => f(a, b)(c) 這種其實叫部分函數應用
柯里化可以實現部分函數應用,但是柯里化不等於部分函數應用
 

參考文檔

二、組合 - compose

組合指的是將多個函數組合成一個函數,這樣一個函數的輸出就可以作爲另一個函數的輸入,從而實現多個函數的鏈式調用

組合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);

 

四、多多參考 Ramda.js

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章