前端JavaScript徹底弄懂函數(shù)柯里化curry

一、什么是柯里化( curry)
在數(shù)學(xué)和計(jì)算機(jī)科學(xué)中,柯里化是一種將使用多個(gè)參數(shù)的一個(gè)函數(shù)轉(zhuǎn)換成一系列使用一個(gè)參數(shù)的函數(shù)的技術(shù)。
舉例來(lái)說(shuō),一個(gè)接收3個(gè)參數(shù)的普通函數(shù),在進(jìn)行柯里化后, 柯里化版本的函數(shù)接收一個(gè)參數(shù)并返回接收下一個(gè)參數(shù)的函數(shù), 該函數(shù)返回一個(gè)接收第三個(gè)參數(shù)的函數(shù)。 最后一個(gè)函數(shù)在接收第三個(gè)參數(shù)后, 將之前接收到的三個(gè)參數(shù)應(yīng)用于原普通函數(shù)中,并返回最終結(jié)果。
數(shù)學(xué)和計(jì)算科學(xué)中的柯里化:
// 數(shù)學(xué)和計(jì)算科學(xué)中的柯里化:
//一個(gè)接收三個(gè)參數(shù)的普通函數(shù)
function sum(a,b,c) {
console.log(a+b+c)
}
//用于將普通函數(shù)轉(zhuǎn)化為柯里化版本的工具函數(shù)
function curry(fn) {
//...內(nèi)部實(shí)現(xiàn)省略,返回一個(gè)新函數(shù)
}
//獲取一個(gè)柯里化后的函數(shù)
let _sum = curry(sum);
//返回一個(gè)接收第二個(gè)參數(shù)的函數(shù)
let A = _sum(1);
//返回一個(gè)接收第三個(gè)參數(shù)的函數(shù)
let B = A(2);
//接收到最后一個(gè)參數(shù),將之前所有的參數(shù)應(yīng)用到原函數(shù)中,并運(yùn)行
B(3) // print : 6
而對(duì)于Javascript語(yǔ)言來(lái)說(shuō),我們通常說(shuō)的柯里化函數(shù)的概念,與數(shù)學(xué)和計(jì)算機(jī)科學(xué)中的柯里化的概念并不完全一樣。
在數(shù)學(xué)和計(jì)算機(jī)科學(xué)中的柯里化函數(shù),一次只能傳遞一個(gè)參數(shù);
而我們Javascript實(shí)際應(yīng)用中的柯里化函數(shù),可以傳遞一個(gè)或多個(gè)參數(shù)。
來(lái)看這個(gè)例子:
//普通函數(shù)
function fn(a,b,c,d,e) {
console.log(a,b,c,d,e)
}
//生成的柯里化函數(shù)
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
對(duì)于已經(jīng)柯里化后的 _fn 函數(shù)來(lái)說(shuō),當(dāng)接收的參數(shù)數(shù)量與原函數(shù)的形參數(shù)量相同時(shí),執(zhí)行原函數(shù); 當(dāng)接收的參數(shù)數(shù)量小于原函數(shù)的形參數(shù)量時(shí),返回一個(gè)函數(shù)用于接收剩余的參數(shù),直至接收的參數(shù)數(shù)量與形參數(shù)量一致,執(zhí)行原函數(shù)。
當(dāng)我們知道柯里化是什么了的時(shí)候,我們來(lái)看看柯里化到底有什么用?
二、柯里化的用途
柯里化實(shí)際是把簡(jiǎn)答的問(wèn)題復(fù)雜化了,但是復(fù)雜化的同時(shí),我們?cè)谑褂煤瘮?shù)時(shí)擁有了更加多的自由度。 而這里對(duì)于函數(shù)參數(shù)的自由處理,正是柯里化的核心所在。 柯里化本質(zhì)上是降低通用性,提高適用性。來(lái)看一個(gè)例子:
我們工作中會(huì)遇到各種需要通過(guò)正則檢驗(yàn)的需求,比如校驗(yàn)電話號(hào)碼、校驗(yàn)郵箱、校驗(yàn)身份證號(hào)、校驗(yàn)密碼等, 這時(shí)我們會(huì)封裝一個(gè)通用函數(shù) checkByRegExp ,接收兩個(gè)參數(shù),校驗(yàn)的正則對(duì)象和待校驗(yàn)的字符串
function checkByRegExp(regExp,string) {
return regExp.test(string);
}
checkByRegExp(/^1\d{10}$/, '18642838455'); // 校驗(yàn)電話號(hào)碼
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com'); // 校驗(yàn)郵箱
上面這段代碼,乍一看沒什么問(wèn)題,可以滿足我們所有通過(guò)正則檢驗(yàn)的需求。 但是我們考慮這樣一個(gè)問(wèn)題,如果我們需要校驗(yàn)多個(gè)電話號(hào)碼或者校驗(yàn)多個(gè)郵箱呢?
我們可能會(huì)這樣做:
checkByRegExp(/^1\d{10}$/, '18642838455'); // 校驗(yàn)電話號(hào)碼
checkByRegExp(/^1\d{10}$/, '13109840560'); // 校驗(yàn)電話號(hào)碼
checkByRegExp(/^1\d{10}$/, '13204061212'); // 校驗(yàn)電話號(hào)碼
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com'); // 校驗(yàn)郵箱
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@qq.com'); // 校驗(yàn)郵箱
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@gmail.com'); // 校驗(yàn)郵箱
我們每次進(jìn)行校驗(yàn)的時(shí)候都需要輸入一串正則,再校驗(yàn)同一類型的數(shù)據(jù)時(shí),相同的正則我們需要寫多次, 這就導(dǎo)致我們?cè)谑褂玫臅r(shí)候效率低下,并且由于 checkByRegExp 函數(shù)本身是一個(gè)工具函數(shù)并沒有任何意義, 一段時(shí)間后我們重新來(lái)看這些代碼時(shí),如果沒有注釋,我們必須通過(guò)檢查正則的內(nèi)容, 我們才能知道我們校驗(yàn)的是電話號(hào)碼還是郵箱,還是別的什么。
此時(shí),我們可以借助柯里化對(duì) checkByRegExp 函數(shù)進(jìn)行封裝,以簡(jiǎn)化代碼書寫,提高代碼可讀性。
//進(jìn)行柯里化
let _check = curry(checkByRegExp);
//生成工具函數(shù),驗(yàn)證電話號(hào)碼
let checkCellPhone = _check(/^1\d{10}$/);
//生成工具函數(shù),驗(yàn)證郵箱
let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
checkCellPhone('18642838455'); // 校驗(yàn)電話號(hào)碼
checkCellPhone('13109840560'); // 校驗(yàn)電話號(hào)碼
checkCellPhone('13204061212'); // 校驗(yàn)電話號(hào)碼
checkEmail('test@163.com'); // 校驗(yàn)郵箱
checkEmail('test@qq.com'); // 校驗(yàn)郵箱
checkEmail('test@gmail.com'); // 校驗(yàn)郵箱
再來(lái)看看通過(guò)柯里化封裝后,我們的代碼是不是變得又簡(jiǎn)潔又直觀了呢。
經(jīng)過(guò)柯里化后,我們生成了兩個(gè)函數(shù) checkCellPhone 和 checkEmail, checkCellPhone 函數(shù)只能驗(yàn)證傳入的字符串是否是電話號(hào)碼, checkEmail 函數(shù)只能驗(yàn)證傳入的字符串是否是郵箱, 它們與 原函數(shù) checkByRegExp 相比,從功能上通用性降低了,但適用性提升了。 柯里化的這種用途可以被理解為:參數(shù)復(fù)用
我們?cè)賮?lái)看一個(gè)例子
假定我們有這樣一段數(shù)據(jù):
let list = [
{
name:'lucy'
},
{
name:'jack'
}
]
我們需要獲取數(shù)據(jù)中的所有 name 屬性的值,常規(guī)思路下,我們會(huì)這樣實(shí)現(xiàn):
let names = list.map(function(item) {
return item.name;
})
那么我們?nèi)绾斡每吕锘乃季S來(lái)實(shí)現(xiàn)呢
let prop = curry(function(key,obj) {
return obj[key];
})
let names = list.map(prop('name'))
看到這里,可能會(huì)有疑問(wèn),這么簡(jiǎn)單的例子,僅僅只是為了獲取 name 的屬性值,為何還要實(shí)現(xiàn)一個(gè) prop 函數(shù)呢,這樣太麻煩了吧。
我們可以換個(gè)思路,prop 函數(shù)實(shí)現(xiàn)一次后,以后是可以多次使用的,所以我們?cè)诳紤]代碼復(fù)雜程度的時(shí)候,是可以將 prop 函數(shù)的實(shí)現(xiàn)去掉的。
我們實(shí)際的代碼可以理解為只有一行 let names = list.map(prop('name'))
這么看來(lái),通過(guò)柯里化的方式,我們的代碼是不是變得更精簡(jiǎn)了,并且可讀性更高了呢。
三、如何封裝柯里化工具函數(shù)
接下來(lái),我們來(lái)思考如何實(shí)現(xiàn) curry 函數(shù)。
回想之前我們對(duì)于柯里化的定義,接收一部分參數(shù),返回一個(gè)函數(shù)接收剩余參數(shù),接收足夠參數(shù)后,執(zhí)行原函數(shù)。
我們已經(jīng)知道了,當(dāng)柯里化函數(shù)接收到足夠參數(shù)后,就會(huì)執(zhí)行原函數(shù),那么我們?nèi)绾稳ゴ_定何時(shí)達(dá)到足夠的參數(shù)呢?
我們有兩種思路:
- 通過(guò)函數(shù)的 length 屬性,獲取函數(shù)的形參個(gè)數(shù),形參的個(gè)數(shù)就是所需的參數(shù)個(gè)數(shù)
- 在調(diào)用柯里化工具函數(shù)時(shí),手動(dòng)指定所需的參數(shù)個(gè)數(shù)
我們將這兩點(diǎn)結(jié)合以下,實(shí)現(xiàn)一個(gè)簡(jiǎn)單 curry 函數(shù):
/**
* 將函數(shù)柯里化
* @param fn 待柯里化的原函數(shù)
* @param len 所需的參數(shù)個(gè)數(shù),默認(rèn)為原函數(shù)的形參個(gè)數(shù)
*/
function curry(fn,len = fn.length) {
return _curry.call(this,fn,len)
}
/**
* 中轉(zhuǎn)函數(shù)
* @param fn 待柯里化的原函數(shù)
* @param len 所需的參數(shù)個(gè)數(shù)
* @param args 已接收的參數(shù)列表
*/
function _curry(fn,len,...args) {
return function (...params) {
let _args = [...args,...params];
if(_args.length >= len){
return fn.apply(this,_args);
}else{
return _curry.call(this,fn,len,..._args)
}
}
}
我們來(lái)驗(yàn)證一下:
let _fn = curry(function(a,b,c,d,e){
console.log(a,b,c,d,e)
});
_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
我們常用的工具庫(kù) lodash 也提供了 curry 方法,并且增加了非常好玩的 placeholder 功能,通過(guò)占位符的方式來(lái)改變傳入?yún)?shù)的順序。
比如說(shuō),我們傳入一個(gè)占位符,本次調(diào)用傳遞的參數(shù)略過(guò)占位符, 占位符所在的位置由下次調(diào)用的參數(shù)來(lái)填充,比如這樣:
直接看一下官網(wǎng)的例子:

接下來(lái)我們來(lái)思考,如何實(shí)現(xiàn)占位符的功能。
對(duì)于 lodash 的 curry 函數(shù)來(lái)說(shuō),curry 函數(shù)掛載在 lodash 對(duì)象上,所以將 lodash 對(duì)象當(dāng)做默認(rèn)占位符來(lái)使用。
而我們的自己實(shí)現(xiàn)的 curry 函數(shù),本身并沒有掛載在任何對(duì)象上,所以將 curry 函數(shù)當(dāng)做默認(rèn)占位符
使用占位符,目的是改變參數(shù)傳遞的順序,所以在 curry 函數(shù)實(shí)現(xiàn)中,每次需要記錄是否使用了占位符,并且記錄占位符所代表的參數(shù)位置。
直接上代碼:
/**
* @param fn 待柯里化的函數(shù)
* @param length 需要的參數(shù)個(gè)數(shù),默認(rèn)為函數(shù)的形參個(gè)數(shù)
* @param holder 占位符,默認(rèn)當(dāng)前柯里化函數(shù)
* @return {Function} 柯里化后的函數(shù)
*/
function curry(fn,length = fn.length,holder = curry){
return _curry.call(this,fn,length,holder,[],[])
}
/**
* 中轉(zhuǎn)函數(shù)
* @param fn 柯里化的原函數(shù)
* @param length 原函數(shù)需要的參數(shù)個(gè)數(shù)
* @param holder 接收的占位符
* @param args 已接收的參數(shù)列表
* @param holders 已接收的占位符位置列表
* @return {Function} 繼續(xù)柯里化的函數(shù) 或 最終結(jié)果
*/
function _curry(fn,length,holder,args,holders){
return function(..._args){
//將參數(shù)復(fù)制一份,避免多次操作同一函數(shù)導(dǎo)致參數(shù)混亂
let params = args.slice();
//將占位符位置列表復(fù)制一份,新增加的占位符增加至此
let _holders = holders.slice();
//循環(huán)入?yún)ⅲ芳訁?shù) 或 替換占位符
_args.forEach((arg,i)=>{
//真實(shí)參數(shù) 之前存在占位符 將占位符替換為真實(shí)參數(shù)
if (arg !== holder && holders.length) {
let index = holders.shift();
_holders.splice(_holders.indexOf(index),1);
params[index] = arg;
}
//真實(shí)參數(shù) 之前不存在占位符 將參數(shù)追加到參數(shù)列表中
else if(arg !== holder && !holders.length){
params.push(arg);
}
//傳入的是占位符,之前不存在占位符 記錄占位符的位置
else if(arg === holder && !holders.length){
params.push(arg);
_holders.push(params.length - 1);
}
//傳入的是占位符,之前存在占位符 刪除原占位符位置
else if(arg === holder && holders.length){
holders.shift();
}
});
// params 中前 length 條記錄中不包含占位符,執(zhí)行函數(shù)
if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
return fn.apply(this,params);
}else{
return _curry.call(this,fn,length,holder,params,_holders)
}
}
}
驗(yàn)證一下:
let fn = function(a, b, c, d, e) {
console.log([a, b, c, d, e]);
}
let _ = {}; // 定義占位符
let _fn = curry(fn,5,_); // 將函數(shù)柯里化,指定所需的參數(shù)個(gè)數(shù),指定所需的占位符
_fn(1, 2, 3, 4, 5); // print: 1,2,3,4,5
_fn(_, 2, 3, 4, 5)(1); // print: 1,2,3,4,5
_fn(1, _, 3, 4, 5)(2); // print: 1,2,3,4,5
_fn(1, _, 3)(_, 4,_)(2)(5); // print: 1,2,3,4,5
_fn(1, _, _, 4)(_, 3)(2)(5); // print: 1,2,3,4,5
_fn(_, 2)(_, _, 4)(1)(3)(5); // print: 1,2,3,4,5
我們已經(jīng)完整實(shí)現(xiàn)了一個(gè) curry 函數(shù)~~
到此這篇關(guān)于前端JavaScript徹底弄懂函數(shù)柯里化的文章就介紹到這了,更多相關(guān)JavaScript函數(shù)柯里化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript中的箭頭函數(shù)基礎(chǔ)語(yǔ)法及使用場(chǎng)景示例
這篇文章主要為大家介紹了?javascript中的箭頭函數(shù)基礎(chǔ)語(yǔ)法及使用場(chǎng)景示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
實(shí)現(xiàn)基于飛書webhook監(jiān)聽github代碼提交
這篇文章主要為大家介紹了實(shí)現(xiàn)基于飛書webhook監(jiān)聽github代碼提交示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
微信小程序page的生命周期和音頻播放及監(jiān)聽實(shí)例詳解
這篇文章主要介紹了微信小程序page的生命周期和音頻播放及監(jiān)聽實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-04-04
微信小程序 動(dòng)態(tài)傳參實(shí)例詳解
這篇文章主要介紹了微信小程序 動(dòng)態(tài)傳參實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-04-04
微信小程序動(dòng)態(tài)的加載數(shù)據(jù)實(shí)例代碼
這篇文章主要介紹了 微信小程序動(dòng)態(tài)的加載數(shù)據(jù)實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-04-04

