JavaScript:函式柯里化
什麼是js柯里化(curry)?
在數學和電腦科學中,柯里化是一種將使用多個引數的一個函式轉換成一系列使用一個引數的函式的技術。
舉例來說,一個接收3個引數的普通函式,在進行柯里化後,柯里化版本的函式接收一個引數並返回接收下一個引數的函式,該函式返回一個接收第三個引數的函式。最後一個函式在接收第三個引數後,
將之前接收到的三個引數應用於原普通函式中,並返回最終結果。
來看這個例子:
//普通函式 function fn(a,b,c,d,e) { console.log(a,b,c,d,e) } //生成的柯里化函式 let _fn = curry(fn); _fn(1,2,3,4,5); // print: 1,2,3,4,5 _fn(1)(2)(3,4,5); // print: 1,2,3,4,5 _fn(1,2)(3,4)(5); // print: 1,2,3,4,5 _fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5
對於已經柯里化後的 _fn 函式來說,當接收的引數數量與原函式的形引數量相同時,執行原函式;
當接收的引數數量小於原函式的形引數量時,返回一個函式用於接收剩餘的引數,直至接收的引數數量與形引數量一致,執行原函式。
當我們知道柯里化是什麼了的時候,我們來看看柯里化到底有什麼用?
柯里化的用途
柯里化實際是把簡單的問題複雜化了,但是複雜化的同時,我們在使用函式時擁有了更加多的自由度。
而這裡對於函式引數的自由處理,正是柯里化的核心所在。
柯里化本質上是降低通用性,提高適用性。來看一個例子:
我們工作中會遇到各種需要通過正則檢驗的需求,比如校驗電話號碼、校驗郵箱、校驗身份證號、校驗密碼等,
這時我們會封裝一個通用函式 checkByRegExp ,接收兩個引數,校驗的正則物件和待校驗的字串
function checkByRegExp(regExp,string) {
return regExp.test(string);
}
checkByRegExp(/^1\d{10}$/, '18642838455'); // 校驗電話號碼
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, '[email protected]'); // 校驗郵箱
上面這段程式碼,乍一看沒什麼問題,可以滿足我們所有通過正則檢驗的需求。 但是我們考慮這樣一個問題,如果我們需要校驗多個電話號碼或者校驗多個郵箱呢?
我們可能會這樣做:
checkByRegExp(/^1\d{10}$/, '18642838455'); // 校驗電話號碼 checkByRegExp(/^1\d{10}$/, '13109840560'); // 校驗電話號碼 checkByRegExp(/^1\d{10}$/, '13204061212'); // 校驗電話號碼 checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, '[email protected]'); // 校驗郵箱 checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, '[email protected]'); // 校驗郵箱 checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, '[email protected]'); // 校驗郵箱
我們每次進行校驗的時候都需要輸入一串正則,再校驗同一型別的資料時,相同的正則我們需要寫多次,
這就導致我們在使用的時候效率低下,並且由於 checkByRegExp 函式本身是一個工具函式並沒有任何意義,
一段時間後我們重新來看這些程式碼時,如果沒有註釋,我們必須通過檢查正則的內容,
我們才能知道我們校驗的是電話號碼還是郵箱,還是別的什麼。
此時,我們可以藉助柯里化對 checkByRegExp 函式進行封裝,以簡化程式碼書寫,提高程式碼可讀性。
//進行柯里化
let _check = curry(checkByRegExp);
//生成工具函式,驗證電話號碼
let checkCellPhone = _check(/^1\d{10}$/);
//生成工具函式,驗證郵箱
let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
checkCellPhone('18642838455'); // 校驗電話號碼
checkCellPhone('13109840560'); // 校驗電話號碼
checkCellPhone('13204061212'); // 校驗電話號碼
checkEmail('[email protected]'); // 校驗郵箱
checkEmail('[email protected]'); // 校驗郵箱
checkEmail('[email protected]'); // 校驗郵箱
再來看看通過柯里化封裝後,我們的程式碼是不是變得又簡潔又直觀了呢。
經過柯里化後,我們生成了兩個函式 checkCellPhone 和 checkEmail,
checkCellPhone 函式只能驗證傳入的字串是否是電話號碼,
checkEmail 函式只能驗證傳入的字串是否是郵箱,
它們與 原函式 checkByRegExp 相比,從功能上通用性降低了,但適用性提升了。
柯里化的這種用途可以被理解為:引數複用
如何封裝柯里化工具函式
接下來,我們來思考如何實現 curry 函式。
回想之前我們對於柯里化的定義,接收一部分引數,返回一個函式接收剩餘引數,接收足夠引數後,執行原函式。
我們已經知道了,當柯里化函式接收到足夠引數後,就會執行原函式,那麼我們如何去確定何時達到足夠的引數呢?
我們有兩種思路:
通過函式的 length 屬性,獲取函式的形參個數,形參的個數就是所需的引數個數
在呼叫柯里化工具函式時,手動指定所需的引數個數
我們將這兩點結合以下,實現一個簡單 curry 函式:
var foo = function(a,b,c){
console.log(a,b,c)
}
var curry= function(fn,args){
var args = args || []
var len = fn.length
return function(){
var _args = args.concat(Array.prototype.slice.call(arguments,0))
if(_args.length>=len){
fn.apply(this,_args)
}else{
return curry.call(this,fn,_args)
}
}
}
var f = curry(foo)
f(1,2,3) // 123
f(2,4)(5) // 245
f(1)(2)(3) // 123
還有很多有趣的內容,比如佔位符。
參考文章
1.https://juejin.cn/post/6844903882208837645#comment
2.https://github.com/mqyqingfeng/Blog/issues/42