前端JavaScript徹底弄懂函數(shù)柯里化curry
一、什么是柯里化( curry)
在數(shù)學和計算機科學中,柯里化是一種將使用多個參數(shù)的一個函數(shù)轉換成一系列使用一個參數(shù)的函數(shù)的技術。
舉例來說,一個接收3個參數(shù)的普通函數(shù),在進行柯里化后, 柯里化版本的函數(shù)接收一個參數(shù)并返回接收下一個參數(shù)的函數(shù), 該函數(shù)返回一個接收第三個參數(shù)的函數(shù)。 最后一個函數(shù)在接收第三個參數(shù)后, 將之前接收到的三個參數(shù)應用于原普通函數(shù)中,并返回最終結果。
數(shù)學和計算科學中的柯里化:
// 數(shù)學和計算科學中的柯里化: //一個接收三個參數(shù)的普通函數(shù) function sum(a,b,c) { console.log(a+b+c) } //用于將普通函數(shù)轉化為柯里化版本的工具函數(shù) function curry(fn) { //...內部實現(xiàn)省略,返回一個新函數(shù) } //獲取一個柯里化后的函數(shù) let _sum = curry(sum); //返回一個接收第二個參數(shù)的函數(shù) let A = _sum(1); //返回一個接收第三個參數(shù)的函數(shù) let B = A(2); //接收到最后一個參數(shù),將之前所有的參數(shù)應用到原函數(shù)中,并運行 B(3) // print : 6
而對于Javascript
語言來說,我們通常說的柯里化函數(shù)的概念,與數(shù)學和計算機科學中的柯里化的概念并不完全一樣。
在數(shù)學和計算機科學中的柯里化函數(shù),一次只能傳遞一個參數(shù);
而我們Javascript
實際應用中的柯里化函數(shù),可以傳遞一個或多個參數(shù)。
來看這個例子:
//普通函數(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
對于已經柯里化后的 _fn
函數(shù)來說,當接收的參數(shù)數(shù)量與原函數(shù)的形參數(shù)量相同時,執(zhí)行原函數(shù); 當接收的參數(shù)數(shù)量小于原函數(shù)的形參數(shù)量時,返回一個函數(shù)用于接收剩余的參數(shù),直至接收的參數(shù)數(shù)量與形參數(shù)量一致,執(zhí)行原函數(shù)。
當我們知道柯里化是什么了的時候,我們來看看柯里化到底有什么用?
二、柯里化的用途
柯里化實際是把簡答的問題復雜化了,但是復雜化的同時,我們在使用函數(shù)時擁有了更加多的自由度。 而這里對于函數(shù)參數(shù)的自由處理,正是柯里化的核心所在。 柯里化本質上是降低通用性,提高適用性。來看一個例子:
我們工作中會遇到各種需要通過正則檢驗的需求,比如校驗電話號碼、校驗郵箱、校驗身份證號、校驗密碼等, 這時我們會封裝一個通用函數(shù) checkByRegExp
,接收兩個參數(shù),校驗的正則對象和待校驗的字符串
function checkByRegExp(regExp,string) { return regExp.test(string); } checkByRegExp(/^1\d{10}$/, '18642838455'); // 校驗電話號碼 checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com'); // 校驗郵箱
上面這段代碼,乍一看沒什么問題,可以滿足我們所有通過正則檢驗的需求。 但是我們考慮這樣一個問題,如果我們需要校驗多個電話號碼或者校驗多個郵箱呢?
我們可能會這樣做:
checkByRegExp(/^1\d{10}$/, '18642838455'); // 校驗電話號碼 checkByRegExp(/^1\d{10}$/, '13109840560'); // 校驗電話號碼 checkByRegExp(/^1\d{10}$/, '13204061212'); // 校驗電話號碼 checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com'); // 校驗郵箱 checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@qq.com'); // 校驗郵箱 checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@gmail.com'); // 校驗郵箱
我們每次進行校驗的時候都需要輸入一串正則,再校驗同一類型的數(shù)據(jù)時,相同的正則我們需要寫多次, 這就導致我們在使用的時候效率低下,并且由于 checkByRegExp
函數(shù)本身是一個工具函數(shù)并沒有任何意義, 一段時間后我們重新來看這些代碼時,如果沒有注釋,我們必須通過檢查正則的內容, 我們才能知道我們校驗的是電話號碼還是郵箱,還是別的什么。
此時,我們可以借助柯里化對 checkByRegExp
函數(shù)進行封裝,以簡化代碼書寫,提高代碼可讀性。
//進行柯里化 let _check = curry(checkByRegExp); //生成工具函數(shù),驗證電話號碼 let checkCellPhone = _check(/^1\d{10}$/); //生成工具函數(shù),驗證郵箱 let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/); checkCellPhone('18642838455'); // 校驗電話號碼 checkCellPhone('13109840560'); // 校驗電話號碼 checkCellPhone('13204061212'); // 校驗電話號碼 checkEmail('test@163.com'); // 校驗郵箱 checkEmail('test@qq.com'); // 校驗郵箱 checkEmail('test@gmail.com'); // 校驗郵箱
再來看看通過柯里化封裝后,我們的代碼是不是變得又簡潔又直觀了呢。
經過柯里化后,我們生成了兩個函數(shù) checkCellPhone 和 checkEmail, checkCellPhone
函數(shù)只能驗證傳入的字符串是否是電話號碼, checkEmail
函數(shù)只能驗證傳入的字符串是否是郵箱, 它們與 原函數(shù) checkByRegExp
相比,從功能上通用性降低了,但適用性提升了。 柯里化的這種用途可以被理解為:參數(shù)復用
我們再來看一個例子
假定我們有這樣一段數(shù)據(jù):
let list = [ { name:'lucy' }, { name:'jack' } ]
我們需要獲取數(shù)據(jù)中的所有 name 屬性的值,常規(guī)思路下,我們會這樣實現(xiàn):
let names = list.map(function(item) { return item.name; })
那么我們如何用柯里化的思維來實現(xiàn)呢
let prop = curry(function(key,obj) { return obj[key]; }) let names = list.map(prop('name'))
看到這里,可能會有疑問,這么簡單的例子,僅僅只是為了獲取 name
的屬性值,為何還要實現(xiàn)一個 prop
函數(shù)呢,這樣太麻煩了吧。
我們可以換個思路,prop
函數(shù)實現(xiàn)一次后,以后是可以多次使用的,所以我們在考慮代碼復雜程度的時候,是可以將 prop
函數(shù)的實現(xiàn)去掉的。
我們實際的代碼可以理解為只有一行 let names = list.map(prop('name'))
這么看來,通過柯里化的方式,我們的代碼是不是變得更精簡了,并且可讀性更高了呢。
三、如何封裝柯里化工具函數(shù)
接下來,我們來思考如何實現(xiàn) curry
函數(shù)。
回想之前我們對于柯里化的定義,接收一部分參數(shù),返回一個函數(shù)接收剩余參數(shù),接收足夠參數(shù)后,執(zhí)行原函數(shù)。
我們已經知道了,當柯里化函數(shù)接收到足夠參數(shù)后,就會執(zhí)行原函數(shù),那么我們如何去確定何時達到足夠的參數(shù)呢?
我們有兩種思路:
- 通過函數(shù)的 length 屬性,獲取函數(shù)的形參個數(shù),形參的個數(shù)就是所需的參數(shù)個數(shù)
- 在調用柯里化工具函數(shù)時,手動指定所需的參數(shù)個數(shù)
我們將這兩點結合以下,實現(xiàn)一個簡單 curry
函數(shù):
/** * 將函數(shù)柯里化 * @param fn 待柯里化的原函數(shù) * @param len 所需的參數(shù)個數(shù),默認為原函數(shù)的形參個數(shù) */ function curry(fn,len = fn.length) { return _curry.call(this,fn,len) } /** * 中轉函數(shù) * @param fn 待柯里化的原函數(shù) * @param len 所需的參數(shù)個數(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) } } }
我們來驗證一下:
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
我們常用的工具庫 lodash
也提供了 curry
方法,并且增加了非常好玩的 placeholder
功能,通過占位符的方式來改變傳入參數(shù)的順序。
比如說,我們傳入一個占位符,本次調用傳遞的參數(shù)略過占位符, 占位符所在的位置由下次調用的參數(shù)來填充,比如這樣:
直接看一下官網的例子:
接下來我們來思考,如何實現(xiàn)占位符的功能。
對于 lodash
的 curry
函數(shù)來說,curry
函數(shù)掛載在 lodash
對象上,所以將 lodash
對象當做默認占位符來使用。
而我們的自己實現(xiàn)的 curry
函數(shù),本身并沒有掛載在任何對象上,所以將 curry
函數(shù)當做默認占位符
使用占位符,目的是改變參數(shù)傳遞的順序,所以在 curry
函數(shù)實現(xiàn)中,每次需要記錄是否使用了占位符,并且記錄占位符所代表的參數(shù)位置。
直接上代碼:
/** * @param fn 待柯里化的函數(shù) * @param length 需要的參數(shù)個數(shù),默認為函數(shù)的形參個數(shù) * @param holder 占位符,默認當前柯里化函數(shù) * @return {Function} 柯里化后的函數(shù) */ function curry(fn,length = fn.length,holder = curry){ return _curry.call(this,fn,length,holder,[],[]) } /** * 中轉函數(shù) * @param fn 柯里化的原函數(shù) * @param length 原函數(shù)需要的參數(shù)個數(shù) * @param holder 接收的占位符 * @param args 已接收的參數(shù)列表 * @param holders 已接收的占位符位置列表 * @return {Function} 繼續(xù)柯里化的函數(shù) 或 最終結果 */ function _curry(fn,length,holder,args,holders){ return function(..._args){ //將參數(shù)復制一份,避免多次操作同一函數(shù)導致參數(shù)混亂 let params = args.slice(); //將占位符位置列表復制一份,新增加的占位符增加至此 let _holders = holders.slice(); //循環(huán)入參,追加參數(shù) 或 替換占位符 _args.forEach((arg,i)=>{ //真實參數(shù) 之前存在占位符 將占位符替換為真實參數(shù) if (arg !== holder && holders.length) { let index = holders.shift(); _holders.splice(_holders.indexOf(index),1); params[index] = arg; } //真實參數(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) } } }
驗證一下:
let fn = function(a, b, c, d, e) { console.log([a, b, c, d, e]); } let _ = {}; // 定義占位符 let _fn = curry(fn,5,_); // 將函數(shù)柯里化,指定所需的參數(shù)個數(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
我們已經完整實現(xiàn)了一個 curry 函數(shù)~~
到此這篇關于前端JavaScript徹底弄懂函數(shù)柯里化的文章就介紹到這了,更多相關JavaScript函數(shù)柯里化內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
javascript中的箭頭函數(shù)基礎語法及使用場景示例
這篇文章主要為大家介紹了?javascript中的箭頭函數(shù)基礎語法及使用場景示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07實現(xiàn)基于飛書webhook監(jiān)聽github代碼提交
這篇文章主要為大家介紹了實現(xiàn)基于飛書webhook監(jiān)聽github代碼提交示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01微信小程序page的生命周期和音頻播放及監(jiān)聽實例詳解
這篇文章主要介紹了微信小程序page的生命周期和音頻播放及監(jiān)聽實例詳解的相關資料,需要的朋友可以參考下2017-04-04微信小程序動態(tài)的加載數(shù)據(jù)實例代碼
這篇文章主要介紹了 微信小程序動態(tài)的加載數(shù)據(jù)實例代碼的相關資料,需要的朋友可以參考下2017-04-04