淺談柯里化函式
首先看看柯里化到底是什麼?
維基百科上說道:柯里化,英語:Currying(果然是滿滿的英譯中的既視感),是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數而且返回結果的新函式的技術。
看這個解釋有一點抽象,我們就拿被做了無數次示例的add函式,來做一個簡單的實現。
// 普通的add函式 function add(x, y) { return x + y } // Currying後 function curryingAdd(x) { return function (y) { return x + y } } add(1, 2) // 3 curryingAdd(1)(2) // 3
實際上就是把add函式的x,y兩個引數變成了先用一個函式接收x然後返回一個函式去處理y引數。現在思路應該就比較清晰了,就是隻傳遞給函式一部分引數來呼叫它,讓它返回一個函式去處理剩下的引數。
但是問題來了費這麼大勁封裝一層,到底有什麼用處呢?沒有好處想讓我們程式設計師多幹事情是不可能滴,這輩子都不可能.
來列一列Currying有哪些好處呢?
1. 引數複用
// 正常正則驗證字串 reg.test(txt) // 函式封裝後 function check(reg, txt) { return reg.test(txt) } check(/\d+/g, 'test') //false check(/[a-z]+/g, 'test') //true // Currying後 function curryingCheck(reg) { return function(txt) { return reg.test(txt) } } var hasNumber = curryingCheck(/\d+/g) var hasLetter = curryingCheck(/[a-z]+/g) hasNumber('test1') // true hasNumber('testtest') // falsehasLetter('21212') // false
上面的示例是一個正則的校驗,正常來說直接呼叫check函式就可以了,但是如果我有很多地方都要校驗是否有數字,其實就是需要將第一個引數reg進行復用,這樣別的地方就能夠直接呼叫hasNumber,hasLetter等函式,讓引數能夠複用,呼叫起來也更方便。
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 || document.addEventListener; if (isSupport) { return element.addEventListener(event, handler, false); } else { return element.attachEvent('on' + event, handler); } }
我們在做專案的過程中,封裝一些dom操作可以說再常見不過,上面第一種寫法也是比較常見,但是我們看看第二種寫法,它相對一第一種寫法就是自執行然後返回一個新的函式,這樣其實就是提前確定了會走哪一個方法,避免每次都進行判斷。
3. 延遲執行
Function.prototype.bind = function (context) { var _this = this var args = Array.prototype.slice.call(arguments, 1) return function() { return _this.apply(context, args) } }
像我們js中經常使用的bind,實現的機制就是Currying.
說了這幾點好處之後,發現還有個問題,難道每次使用Currying都要對底層函式去做修改,
有沒有什麼通用的封裝方法?
// 初步封裝 var currying = function(fn) { // args 獲取第一個方法內的全部引數 var args = Array.prototype.slice.call(arguments, 1) return function() { // 將後面方法裡的全部引數和args進行合併 var newArgs = args.concat(Array.prototype.slice.call(arguments)) // 把合併後的引數通過apply作為fn的引數並執行 return fn.apply(this, newArgs) } }
這邊首先是初步封裝,通過閉包把初步引數給儲存下來,然後通過獲取剩下的arguments進行拼接,最後執行需要currying的函式。
但是好像還有些什麼缺陷,這樣返回的話其實只能多擴充套件一個引數,currying(a)(b)(c)這樣的話,貌似就不支援了(不支援多引數呼叫),一般這種情況都會想到使用遞迴再進行封裝一層。
// 支援多引數傳遞 function progressCurrying(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 progressCurrying.call(_this, fn, _args); } // 引數收集完畢,則執行fn return fn.apply(this, _args); } }
這邊其實是在初步的基礎上,加上了遞迴的呼叫,只要引數個數小於最初的fn.length,就會繼續執行遞迴。
好處說完了,通用方法也有了,讓我們來關注下curry的效能
curry的一些效能問題你只要知道下面四點就差不多了:
- 存取arguments物件通常要比存取命名引數要慢一點
- 一些老版本的瀏覽器在arguments.length的實現上是相當慢的
- 使用fn.apply( … ) 和 fn.call( … )通常比直接呼叫fn( … ) 稍微慢點
- 建立大量巢狀作用域和閉包函式會帶來花銷,無論是在記憶體還是速度上
其實在大部分應用中,主要的效能瓶頸是在操作DOM節點上,這js的效能損耗基本是可以忽略不計的,所以curry是可以直接放心的使用。
最後再擴充套件一道經典面試題
// 實現一個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
作者:flowsands
連結:https://www.jianshu.com/p/2975c25e4d71
來源:簡書