柯里化是函數(shù)的一個(gè)高級(jí)應(yīng)用命斧,想要理解它并不簡(jiǎn)單莽红。因此我一直在思考應(yīng)該如何更加表達(dá)才能讓大家理解起來(lái)更加容易。
通過(guò)上一個(gè)章節(jié)的學(xué)習(xí)我們知道稀余,接收函數(shù)作為參數(shù)的函數(shù)悦冀,都可以叫做高階函數(shù)。我們常常利用高階函數(shù)來(lái)封裝一些公共的邏輯睛琳。
這一章我們要學(xué)習(xí)的柯里化盒蟆,其實(shí)就是高階函數(shù)的一種特殊用法踏烙。
柯里化是指這樣一個(gè)函數(shù)(假設(shè)叫做createCurry),他接收函數(shù)A作為參數(shù)历等,運(yùn)行后能夠返回一個(gè)新的函數(shù)讨惩。并且這個(gè)新的函數(shù)能夠處理函數(shù)A的剩余參數(shù)。
這樣的定義不太好理解寒屯,我們可以通過(guò)下面的例子配合解釋荐捻。
有一個(gè)接收三個(gè)參數(shù)的函數(shù)A。
function A(a, b, c) {
// do something
}
假如寡夹,我們有一個(gè)已經(jīng)封裝好了的柯里化通用函數(shù)createCurry处面。他接收bar作為參數(shù),能夠?qū)轉(zhuǎn)化為柯里化函數(shù)菩掏,返回結(jié)果就是這個(gè)被轉(zhuǎn)化之后的函數(shù)魂角。
var _A = createCurry(A);
那么_A作為createCurry運(yùn)行的返回函數(shù),他能夠處理A的剩余參數(shù)智绸。因此下面的運(yùn)行結(jié)果都是等價(jià)的野揪。
_A(1, 2, 3);
_A(1, 2)(3);
_A(1)(2, 3);
_A(1)(2)(3);
A(1, 2, 3);
函數(shù)A被createCurry轉(zhuǎn)化之后得到柯里化函數(shù)_A,_A能夠處理A的所有剩余參數(shù)瞧栗。因此柯里化也被稱為部分求值斯稳。
在簡(jiǎn)單的場(chǎng)景下,可以不用借助柯里化通用式來(lái)轉(zhuǎn)化得到柯里化函數(shù)沼溜,我們憑借眼力自己封裝平挑。
例如有一個(gè)簡(jiǎn)單的加法函數(shù),他能夠?qū)⒆陨淼娜齻€(gè)參數(shù)加起來(lái)并返回計(jì)算結(jié)果系草。
function add(a, b, c) {
return a + b + c;
}
那么add函數(shù)的柯里化函數(shù)_add則可以如下:
function _add(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}
下面的運(yùn)算方式是等價(jià)的通熄。
add(1, 2, 3);
_add(1)(2)(3);
當(dāng)然,靠眼力封裝的柯里化函數(shù)自由度偏低找都,柯里化通用式具備更加強(qiáng)大的能力唇辨。因此我們需要知道如何去封裝這樣一個(gè)柯里化的通用式。
首先通過(guò)_add可以看出能耻,柯里化函數(shù)的運(yùn)行過(guò)程其實(shí)是一個(gè)參數(shù)的收集過(guò)程赏枚,我們將每一次傳入的參數(shù)收集起來(lái),并在最里層里面處理晓猛。在實(shí)現(xiàn)createCurry時(shí)饿幅,可以借助這個(gè)思路來(lái)進(jìn)行封裝。
封裝如下:
// 簡(jiǎn)單實(shí)現(xiàn)戒职,參數(shù)只能從右到左傳遞
function createCurry(func, args) {
var arity = func.length;
var args = args || [];
return function() {
var _args = [].slice.call(arguments);
[].push.apply(_args, args);
// 如果參數(shù)個(gè)數(shù)小于最初的func.length栗恩,則遞歸調(diào)用,繼續(xù)收集參數(shù)
if (_args.length < arity) {
return createCurry.call(this, func, _args);
}
// 參數(shù)收集完畢洪燥,則執(zhí)行func
return func.apply(this, _args);
}
}
盡管我已經(jīng)做了足夠詳細(xì)的注解磕秤,但是我想理解起來(lái)也并不是那么容易乳乌,因此建議大家用點(diǎn)耐心多閱讀幾遍。這個(gè)createCurry函數(shù)的封裝借助閉包與遞歸市咆,實(shí)現(xiàn)了一個(gè)參數(shù)收集汉操,并在收集完畢之后執(zhí)行所有參數(shù)的一個(gè)過(guò)程。
聰明的讀者可能已經(jīng)發(fā)現(xiàn)蒙兰,把函數(shù)經(jīng)過(guò)createCurry轉(zhuǎn)化為一個(gè)柯里化函數(shù)磷瘤,最后執(zhí)行的結(jié)果,不是正好相當(dāng)于執(zhí)行函數(shù)自身嗎癞己?柯里化是不是把簡(jiǎn)單的問(wèn)題復(fù)雜化了膀斋?
如果你能夠提出這樣的問(wèn)題,那么說(shuō)明你確實(shí)已經(jīng)對(duì)柯里化有了一定的了解痹雅⊙龅#柯里化確實(shí)是把簡(jiǎn)答的問(wèn)題復(fù)雜化了,但是復(fù)雜化的同時(shí)绩社,我們使用函數(shù)擁有了更加多的自由度摔蓝。而這里對(duì)于函數(shù)參數(shù)的自由處理,正是柯里化的核心所在愉耙。
舉一個(gè)非常常見(jiàn)的例子贮尉。
如果我們想要驗(yàn)證一串?dāng)?shù)字是否是正確的手機(jī)號(hào),按照普通的思路來(lái)做朴沿,大家可能是這樣封裝猜谚,如下:
function checkPhone(phoneNumber) {
return /^1[34578]\d{9}$/.test(phoneNumber);
}
而如果想要驗(yàn)證是否是郵箱呢?這么封裝:
function checkEmail(email) {
return /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/.test(email);
}
我們還可能會(huì)遇到驗(yàn)證身份證號(hào)赌渣,驗(yàn)證密碼等各種驗(yàn)證信息魏铅,因此在實(shí)踐中,為了統(tǒng)一邏輯坚芜,我們就會(huì)封裝一個(gè)更為通用的函數(shù)览芳,將用于驗(yàn)證的正則與將要被驗(yàn)證的字符串作為參數(shù)傳入。
function check(targetString, reg) {
return reg.test(targetString);
}
但是這樣封裝之后鸿竖,在使用時(shí)又會(huì)稍微麻煩一點(diǎn)沧竟,因?yàn)闀?huì)總是輸入一串正則,這樣就導(dǎo)致了使用時(shí)的效率低下缚忧。
check(/^1[34578]\d{9}$/, '14900000088');
check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com');
這個(gè)時(shí)候悟泵,我們就可以借助柯里化,在check的基礎(chǔ)上再做一層封裝闪水,以簡(jiǎn)化使用魁袜。
var _check = createCurry(check);
var checkPhone = _check(/^1[34578]\d{9}$/);
var checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
最后在使用的時(shí)候就會(huì)變得更加直觀與簡(jiǎn)潔了。
checkPhone('183888888');
checkEmail('xxxxx@test.com');
經(jīng)過(guò)這個(gè)過(guò)程我們發(fā)現(xiàn)敦第,柯里化能夠應(yīng)對(duì)更加復(fù)雜的邏輯封裝峰弹。當(dāng)情況變得多變,柯里化依然能夠應(yīng)付自如芜果。
雖然柯里化確實(shí)在一定程度上將問(wèn)題復(fù)雜化了鞠呈,也讓代碼更加不容易理解,但是柯里化在面對(duì)復(fù)雜情況下的靈活性卻讓我們不得不愛(ài)右钾。
當(dāng)然這個(gè)案例本身情況還算簡(jiǎn)單蚁吝,所以還不能夠特別明顯的凸顯柯里化的優(yōu)勢(shì),我們的主要目的在于借助這個(gè)案例幫助大家了解柯里化在實(shí)踐中的用途舀射。
繼續(xù)來(lái)思考一個(gè)例子窘茁。這個(gè)例子與map有關(guān)。在高階函數(shù)的章節(jié)中脆烟,我們分析了封裝map方法的思考過(guò)程山林。由于我們沒(méi)有辦法確認(rèn)一個(gè)數(shù)組在遍歷時(shí)會(huì)執(zhí)行什么操作,因此我們只能將調(diào)用for循環(huán)的這個(gè)統(tǒng)一邏輯封裝起來(lái)邢羔,而具體的操作則通過(guò)參數(shù)傳入的形式讓使用者自定義驼抹。這就是map函數(shù)。
但是拜鹤,這是針對(duì)了所有的情況我們才會(huì)這樣想框冀。
實(shí)踐中我們常常會(huì)發(fā)現(xiàn),在我們的某個(gè)項(xiàng)目中敏簿,針對(duì)于某一個(gè)數(shù)組的操作其實(shí)是固定的明也,也就是說(shuō),同樣的操作惯裕,可能會(huì)在項(xiàng)目的不同地方調(diào)用很多次温数。
于是,這個(gè)時(shí)候轻猖,我們就可以在map函數(shù)的基礎(chǔ)上帆吻,進(jìn)行二次封裝,以簡(jiǎn)化我們?cè)陧?xiàng)目中的使用咙边。假如這個(gè)在我們項(xiàng)目中會(huì)調(diào)用多次的操作是將數(shù)組的每一項(xiàng)都轉(zhuǎn)化為百分比 1 --> 100%猜煮。
普通思維下我們可以這樣來(lái)封裝。
function getNewArray(array) {
return array.map(function(item) {
return item * 100 + '%'
})
}
getNewArray([1, 2, 3, 0.12]); // ['100%', '200%', '300%', '12%'];
而如果借助柯里化來(lái)二次封裝這樣的邏輯败许,則會(huì)如下實(shí)現(xiàn):
function _map(func, array) {
return array.map(func);
}
var _getNewArray = createCurry(_map);
var getNewArray = _getNewArray(function(item) {
return item * 100 + '%'
})
getNewArray([1, 2, 3, 0.12]); // ['100%', '200%', '300%', '12%'];
getNewArray([0.01, 1]); // ['1%', '100%']
如果我們的項(xiàng)目中的固定操作是希望對(duì)數(shù)組進(jìn)行一個(gè)過(guò)濾王带,找出數(shù)組中的所有Number類型的數(shù)據(jù)。借助柯里化思維我們可以這樣做市殷。
function _filter(func, array) {
return array.filter(func);
}
var _find = createCurry(_filter);
var findNumber = _find(function(item) {
if (typeof item == 'number') {
return item;
}
})
findNumber([1, 2, 3, '2', '3', 4]); // [1, 2, 3, 4]
// 當(dāng)我們繼續(xù)封裝另外的過(guò)濾操作時(shí)就會(huì)變得非常簡(jiǎn)單
// 找出數(shù)字為20的子項(xiàng)
var find20 = _find(function(item, i) {
if (typeof item === 20) {
return i;
}
})
find20([1, 2, 3, 30, 20, 100]); // 4
// 找出數(shù)組中大于100的所有數(shù)據(jù)
var findGreater100 = _find(function(item) {
if (item > 100) {
return item;
}
})
findGreater100([1, 2, 101, 300, 2, 122]); // [101, 300, 122]
我采用了與check例子不一樣的思維方向來(lái)想大家展示我們?cè)谑褂每吕锘瘯r(shí)的想法愕撰。目的是想告訴大家,柯里化能夠幫助我們應(yīng)對(duì)更多更復(fù)雜的場(chǎng)景。
當(dāng)然不得不承認(rèn)搞挣,這些例子都太簡(jiǎn)單了带迟,簡(jiǎn)單到如果使用柯里化的思維來(lái)處理他們顯得有一點(diǎn)多此一舉,而且變得難以理解囱桨。因此我想讀者朋友們也很難從這些例子中感受到柯里化的魅力仓犬。不過(guò)沒(méi)關(guān)系,如果我們能夠通過(guò)這些例子掌握到柯里化的思維舍肠,那就是最好的結(jié)果了搀继。在未來(lái)你的實(shí)踐中,如果你發(fā)現(xiàn)用普通的思維封裝一些邏輯慢慢變得困難翠语,不妨想一想在這里學(xué)到的柯里化思維叽躯,應(yīng)用起來(lái),柯里化足夠強(qiáng)大的自由度一定能給你一個(gè)驚喜肌括。
當(dāng)然也并不建議在任何情況下以炫技為目的的去使用柯里化点骑,在柯里化的實(shí)現(xiàn)中,我們知道柯里化雖然具有了更多的自由度们童,但同時(shí)柯里化通用式里調(diào)用了arguments對(duì)象畔况,使用了遞歸與閉包,因此柯里化的自由度是以犧牲了一定的性能為代價(jià)換來(lái)的慧库。只有在情況變得復(fù)雜時(shí)跷跪,才是柯里化大顯身手的時(shí)候。
額外知識(shí)補(bǔ)充
無(wú)限參數(shù)的柯里化齐板。
該部分內(nèi)容可忽略
在前端面試中吵瞻,你可能會(huì)遇到這樣一個(gè)涉及到柯里化的題目。
// 實(shí)現(xiàn)一個(gè)add方法甘磨,使計(jì)算結(jié)果能夠滿足如下預(yù)期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
這個(gè)題目的目的是想讓add執(zhí)行之后返回一個(gè)函數(shù)能夠繼續(xù)執(zhí)行橡羞,最終運(yùn)算的結(jié)果是所有出現(xiàn)過(guò)的參數(shù)之和。而這個(gè)題目的難點(diǎn)則在于參數(shù)的不固定济舆。我們不知道函數(shù)會(huì)執(zhí)行幾次卿泽。因此我們不能使用上面我們封裝的createCurry的通用公式來(lái)轉(zhuǎn)換一個(gè)柯里化函數(shù)。只能自己封裝滋觉,那么怎么辦呢签夭?在此之前,補(bǔ)充2個(gè)非常重要的知識(shí)點(diǎn)椎侠。
一個(gè)是ES6函數(shù)的不定參數(shù)第租。假如我們有一個(gè)數(shù)組,希望把這個(gè)數(shù)組中所有的子項(xiàng)展開(kāi)傳遞給一個(gè)函數(shù)作為參數(shù)我纪。那么我們應(yīng)該怎么做慎宾?
// 大家可以思考一下丐吓,如果將args數(shù)組的子項(xiàng)展開(kāi)作為add的參數(shù)傳入
function add(a, b, c, d) {
return a + b + c + d;
}
var args = [1, 3, 100, 1];
在ES5中,我們可以借助之前學(xué)過(guò)的apply來(lái)達(dá)到我們的目的趟据。
add.apply(null, args); // 105
而在ES6中券犁,提供了一種新的語(yǔ)法來(lái)解決這個(gè)問(wèn)題,那就是不定參之宿。寫法如下:
add(...args); // 105
這兩種寫法是等效的族操。OK,先記在這里比被。在接下的實(shí)現(xiàn)中,我們會(huì)用到不定參數(shù)的特性泼舱。
第二個(gè)要補(bǔ)充的知識(shí)點(diǎn)是函數(shù)的隱式轉(zhuǎn)換等缀。當(dāng)我們直接將函數(shù)參與其他的計(jì)算時(shí),函數(shù)會(huì)默認(rèn)調(diào)用toString方法娇昙,直接將函數(shù)體轉(zhuǎn)換為字符串參與計(jì)算尺迂。
function fn() { return 20 }
console.log(fn + 10); // 輸出結(jié)果 function fn() { return 20 }10
我們可以重寫函數(shù)的toString方法,讓函數(shù)參與計(jì)算時(shí)冒掌,輸出我們想要的結(jié)果噪裕。
function fn() { return 20; }
fn.toString = function() { return 30 }
console.log(fn + 10); // 40
除此之外,當(dāng)我們重寫函數(shù)的valueOf方法也能夠改變函數(shù)的隱式轉(zhuǎn)換結(jié)果股毫。
function fn() { return 20; }
fn.valueOf = function() { return 60 }
console.log(fn + 10); // 70
當(dāng)我們同時(shí)重寫函數(shù)的toString方法與valueOf方法時(shí)膳音,最終的結(jié)果會(huì)取valueOf方法的返回結(jié)果。
function fn() { return 20; }
fn.valueOf = function() { return 50 }
fn.toString = function() { return 30 }
console.log(fn + 10); // 60
補(bǔ)充了這兩個(gè)知識(shí)點(diǎn)之后铃诬,我們可以來(lái)嘗試完成之前的題目了祭陷。add方法的實(shí)現(xiàn)仍然會(huì)是一個(gè)參數(shù)的收集過(guò)程。當(dāng)add函數(shù)執(zhí)行到最后時(shí)趣席,仍然返回的是一個(gè)函數(shù)兵志,但是我們可以通過(guò)定義toString/valueOf的方式,讓這個(gè)函數(shù)可以直接參與計(jì)算宣肚,并且轉(zhuǎn)換的結(jié)果是我們想要的想罕。而且它本身也仍然可以繼續(xù)執(zhí)行接收新的參數(shù)。實(shí)現(xiàn)方式如下霉涨。
function add() {
// 第一次執(zhí)行時(shí)按价,定義一個(gè)數(shù)組專門用來(lái)存儲(chǔ)所有的參數(shù)
var _args = [].slice.call(arguments);
// 在內(nèi)部聲明一個(gè)函數(shù),利用閉包的特性保存_args并收集所有的參數(shù)值
var adder = function () {
var _adder = function() {
// [].push.apply(_args, [].slice.call(arguments));
_args.push(...arguments);
return _adder;
};
// 利用隱式轉(zhuǎn)換的特性嵌纲,當(dāng)最后執(zhí)行時(shí)隱式轉(zhuǎn)換俘枫,并計(jì)算最終的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
// return adder.apply(null, _args);
return adder(..._args);
}
var a = add(1)(2)(3)(4); // f 10
var b = add(1, 2, 3, 4); // f 10
var c = add(1, 2)(3, 4); // f 10
var d = add(1, 2, 3)(4); // f 10
// 可以利用隱式轉(zhuǎn)換的特性參與計(jì)算
console.log(a + 10); // 20
console.log(b + 20); // 30
console.log(c + 30); // 40
console.log(d + 40); // 50
// 也可以繼續(xù)傳入?yún)?shù),得到的結(jié)果再次利用隱式轉(zhuǎn)換參與計(jì)算
console.log(a(10) + 100); // 120
console.log(b(10) + 100); // 120
console.log(c(10) + 100); // 120
console.log(d(10) + 100); // 120
// 其實(shí)上栗中的add方法逮走,就是下面這個(gè)函數(shù)的柯里化函數(shù)鸠蚪,只不過(guò)我們并沒(méi)有使用通用式來(lái)轉(zhuǎn)化,而是自己封裝
function add(...args) {
return args.reduce((a, b) => a + b);
}
下一篇:前端基礎(chǔ)進(jìn)階(十一):詳解面向?qū)ο蟆?gòu)造函數(shù)茅信、原型與原型鏈
上一篇:前端基礎(chǔ)進(jìn)階(九):函數(shù)與函數(shù)式編程
前端基礎(chǔ)進(jìn)階目錄