前端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)郵箱
上面這段代碼,乍一看沒(méi)什么問(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í),相同的正則我們需要寫(xiě)多次, 這就導(dǎo)致我們?cè)谑褂玫臅r(shí)候效率低下,并且由于 checkByRegExp
函數(shù)本身是一個(gè)工具函數(shù)并沒(méi)有任何意義, 一段時(shí)間后我們重新來(lái)看這些代碼時(shí),如果沒(méi)有注釋,我們必須通過(guò)檢查正則的內(nèi)容, 我們才能知道我們校驗(yàn)的是電話號(hào)碼還是郵箱,還是別的什么。
此時(shí),我們可以借助柯里化對(duì) checkByRegExp
函數(shù)進(jìn)行封裝,以簡(jiǎn)化代碼書(shū)寫(xiě),提高代碼可讀性。
//進(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ù),本身并沒(méi)有掛載在任何對(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)基于飛書(shū)webhook監(jiān)聽(tīng)github代碼提交
這篇文章主要為大家介紹了實(shí)現(xiàn)基于飛書(shū)webhook監(jiān)聽(tīng)github代碼提交示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01微信小程序page的生命周期和音頻播放及監(jiān)聽(tīng)實(shí)例詳解
這篇文章主要介紹了微信小程序page的生命周期和音頻播放及監(jiān)聽(tīng)實(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