零、資料
1. 詳解JS函數柯里化 ;
2. 函數式編的JS: curry ;
一、基礎概念
維基百科上說道:柯里化,英語:Currying(果然是滿滿的英譯中的既視感),是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數而且返回結果的新函數的技術。
舉例:
// 普通的add函數 function multiply(x, y) { return x * y } // Currying 前的調用 multiply(3, 4) // 12 multiply(3, 5) // 15 multiply(3, 6) // 16 經過 Currying(僞) 後 function multiply(x) { return function(y) { return x * y } } multiply(3)(4) // 12
實際上就是把add函數的x,y兩個參數變成了先用一個函數接收x然後返回一個函數去處理y參數。現在思路應該就比較清晰了,就是隻傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。
二、優點
這裏的優點以及代碼基本搬運自資料 1 中內容。
1. 參數複用
// 正則驗證字符串 reg.test(txt); // 很符合我們寫業務代碼的習慣 function check(reg, txt) { return reg.test(txt); } check(/\d+/g, 'test') //false check(/[a-z]+/g, 'test') //true // curry 後 function curringCheck(reg) { return function(txt) { return reg.test(txt) } } // 中間函數, 預先設定規則 var hasNumber = curryingCheck(/\d+/g) var hasLetter = curryingCheck(/[a-z]+/g) // 這樣,想做哪種檢測,直接調用中間函數即可,不需要每次都傳入規則 hasNumber('test1') // true hasNumber('testtest') // false hasLetter('21212') // false // es 6 的簡便寫法, 這裏先用稍微用下下面的內容 // 其中, fn 是待 Currying 的函數, curry 是工具函數 let curry = (fn) => (fArg) => (sArg) => fn(fArg, sArg);
2. 提前確認(這一條更多的是用到了閉包)
// 原代碼 var on = function(element, event, handler) { if (document.addEventListener) { if (element && event && handler) { element.addEventListener(event, handler, false); } } else { if (element && event && handler) { element.attachEvent('on' + event, handler); } } } // 自執行函數形成閉包 var on = (function () { if (document.addEventListener) { return function(element, event, handler) { if (element && event && handler) { element.addEventListener(event, handler, false); } } } else { return function(element, event, handler) { if (element && event && handler) { element.attachEvent('on' + event, handler); } } } })(); // 換一種寫法可能比較好理解一點,上面就是把isSupport這個參數給先確定下來了 var on = function(isSupport, element, event, handler) { isSupport = isSupport || documnet.addEventListener; if (isSupport) { return element.addEventListener(event, handler, false); } else { return element.attachEvent('on' + event, handler); } }
3. 延遲運行(感覺也是閉包啊)
Function.prototype.bind = function(context) { var _this = this; var args = Array.prototype.slice.call(arguments, 1); return functon() { return _this.apply(context, args) } }
三、統一封裝
1. 單層
var currying = function(fn) { // 獲取第一個方法內的全部參數 args = Array.prototype.slice.call(arguments, 1); return function() { // 將後面方法裏的全部參數和 args 拼起來 var newArgs = args.concat(Array.prototype.slice.call(arguments)); return fn.apply(this, newArgs); } }
2. 多層 (遞歸,適合 fn(.)(.)(.)(.)... 的形式)
三層寫法核心思路是一致的。// 寫法 1, 支持多參數傳遞 function currying(fn, args) { var _this = this; var len = fn.length; var args = args || []; return function() { var _args = Array.prototype.slice.call(arguments); Array.prototype.push.apply(args, _args); // 如果參數個數小於最初的fn.length,則遞歸調用,繼續收集參數 if (_args.length < len) { return currying.call(_this, fn, _args); } return fn.apply(this, _args); } } // 寫法 2 let currying = (fn, type = []) => { let len = fn.length; return function(...args) { let concatValue = [...type, ...args]; if (concatValue.length < len) { return currying(fn, concatValue); } else { return fn(...concatValue); } } } // 寫法 3 // 多了層匿名函數, 用來蒐集傳入的參數 let curry = fn => { let len = fn.length; return function curriedFn(...args) { if (args.length < len) { return function () { return curriedFn.apply(null, args.concat([].slice.call(arguments))); } } return fn.apply(null, args); } }
四、缺陷
主要在性能上:
1. 由於使用了閉包, 閉包該有的問題一個不落地全部繼承;
2.(如果有) 存取 arguments 對象通常要比存取命名參數要慢一點;
3.(如果有) 一些老版本的瀏覽器在 arguments.length 的實現上是相當地慢;
4.(如果有) 使用 fn.apply(...) 和 fn.call(...) 通常比直接調用 fn (...) 稍微慢一點;
不過相對於頻繁的 DOM 操作, currying 到來的性能消耗可以忽略不計。
五、拓展與總結
currying 函數在設計上第一次調用必須傳入一個函數,這是它的使用原則。
另外, 如果原函數的參數 > 2 ( fn(x, y, z, m, n, ...) )的話, 多層寫法中的 2 & 3 是支持 curry(fn)(x)(y, z)(m)(n)(...) 這種寫法的。
下面這條題目作爲拓展:
// 實現一個add方法,使計算結果能夠滿足如下預期: add(1)(2)(3) = 6; add(1, 2, 3)(4) = 10; add(1)(2)(3)(4)(5) = 15; function add() { // 第一次執行時,定義一個數組專門用來存儲所有的參數 var _args = Array.prototype.slice.call(arguments); // 在內部聲明一個函數,利用閉包的特性保存_args並收集所有的參數值 var _adder = function() { _args.push(...arguments); return _adder; }; // 利用toString隱式轉換的特性,當最後執行時隱式轉換,並計算最終的值返回 _adder.toString = function () { return _args.reduce(function (a, b) { return a + b; }); } return _adder; } add(1)(2)(3) // 6 add(1, 2, 3)(4) // 10 add(1)(2)(3)(4)(5) // 15 add(2, 6)(1) // 9