以前在 youtube 上看到一個技術視頻,講「underscore.js的接口爲何很差用」,以及什麼樣的接口更好用。演講者是 lodash.js 的做者,他提出了一種「全面函數式」的 js 接口設計模式。大概相似這樣:javascript
// 傳統接口 _.map([1, 2, 3], function (el) {return el * 2}); // return [2, 4, 6] // 函數式接口 var fn = _.map([1, 2, 3]); // return a function fn(function (el) {return el * 2}); // return [2, 4, 6]; // 或者 _.map([1, 2, 3])(function (el) {return el * 2}); // return [2, 4, 6];
找到一點感受沒有?其實就是函數式編程語言中普遍存在的「科裏化」函數。當實參填滿形參表的時候,執行結算返回結果,不然返回一個臨時函數,繼續接受實參。java
看到這個寫法眼前一亮,感受有大規模簡化代碼的潛力。當時實際試了一下發下不少地方用不了,由於以前寫的代碼受 jQuery 影響,有不少這樣的接口:node
foobar.attribute(name); // 讀屬性 foobar.attribute(name, newValue); // 寫屬性
這樣的接口是按照上述方法 curry 化會使得讀屬性變得不可能,根本緣由是參數數量不一樣時 attribute 函數的語義根本不同。使用 jQuery 的時候感受這種寫法很是爽,後來就跟着這麼寫,可是目前看來這樣的接口設計是有問題的。git
言歸正傳,今天聊聊這樣的接口如何實現,以及 lodash 中的 fp 模塊。github
說到底就是個 currying 的問題,currying 在不少語言中是內置功能,可是 js 沒有,因此咱們要實現一個 currying 工具函數。首先貼一個最簡易的 currying 實現,它的功能很是簡單,輸入一個函數 fn1 和部分實參,返回一個保存部分實參,繼續接收實參的函數 fn2,調用fn2,它會合並實參數組,並調用 fn1。算法
/** * 函數柯里化 * @param fn 輸入函數 * @return 柯里化後的函數 */ var curry = function (fn) { if (!isFunction(fn)) { return; } var args = slice(arguments, 1); return function () { return fn.apply(this, args.concat(slice(arguments, 0))); } }
isFunction 和 slice 你們都知道我就不貼了。看一下如何調用:npm
function add(a, b) { return a + b; } addOne = curry(add, 1); addOne(2); // return 3
有時候咱們須要輸入的部分實參是數組列表形式,因此咱們包裝一下剛纔的 curry 函數:編程
/** * 函數柯里化 * @param fn 輸入函數 * @param arr 參數列表 * @return 柯里化後的函數 */ var curryApply = function (fn, arr) { if (!isFunction(fn)) { return; } var args = arr.slice(0); args.unshift(fn); return curry.apply(this, args); }
上面的 curry 函數有個問題,就是連續屢次補充實參,咱們還須要封裝一個支持連續調用的版本:設計模式
/** * 自動柯里化 * @param fn 輸入函數 * @param n 輸入函數參數個數 * @return 柯里化後的函數 */ var autoCurry = function (fn, n) { if (!isFunction(fn)) { return; } function retFn() { var len = arguments.length; var args = slice(arguments, 0); var nextn = n - len; if (nextn > 0) { return autoCurry(curryApply(retFn, args), nextn); } return fn.apply(this, args); } return retFn; }
autoCurry 使用的遞歸的方法,輸出函數能夠能夠經過簡單調用的方式連續補充實參,當實參和預設的參數數量相等時,執行輸入函數。使用方法以下:數組
function compute(a, b, c) { return (a + b) * c; } var curryedCompute = autoCurry(compute, 3); compute(1, 2, 3); // return 9 curryedCompute(1)(2)(3); // return 9
你們若是使用 node.js 的話,可能知道 npm 中有個 curry 模塊,實現的功能是同樣的,不一樣的是當你不輸入參數個數 n 時,curry 模塊 會使用 Function 對象的 length 屬性做爲預設的 n 值。
到這裏實現原理就講清楚了。本着不造輪子的原則,若是你們想嘗試一下函數式風格的基礎 js 庫的話,建議使用 lodash/fp 這個模塊。你們都知道 lodash 是 underscore 的 better implemention,而 lodash/fp 就是科裏化的 lodash。與簡單的 currying 不一樣的是,爲了方便使用,lodash/fp 的設計者調換了一些接口的參數順序,好比開頭提到的 _.map 接口,若是簡單 currying 的話第一個參數應該是數組[1, 2, 3],可是大多數時候,咱們想要持有的是一個算法,用這個算法處理不一樣的數據。因此咱們但願暫存的其實是第二個參數 fn,因此 lodash/fp 的接口是這樣的:
// The `lodash/map` iteratee receives three arguments: // (value, index|key, collection) _.map(['6', '8', '10'], parseInt); // → [6, NaN, 2] // The `lodash/fp/map` iteratee is capped at one argument: // (value) fp.map(parseInt)(['6', '8', '10']); // → [6, 8, 10]
關於 lodash/fp 更詳細的說明,請看:https://github.com/lodash/lodash/wiki/FP...