關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅(jiān)實(shí)的梁柱笆搓;分享性湿,是 CSS 里最閃耀的一瞥;總結(jié)满败,是 JavaScript 中最嚴(yán)謹(jǐn)?shù)倪壿嫹羝怠=?jīng)過(guò)捶打磨練,成就了本書(shū)的中文版算墨。本書(shū)包含了函數(shù)式編程之精髓宵荒,希望可以幫助大家在學(xué)習(xí)函數(shù)式編程的道路上走的更順暢。比心净嘀。
譯者團(tuán)隊(duì)(排名不分先后):阿希报咳、blueken、brucecham挖藏、cfanlife暑刃、dail、kyoko-df膜眠、l3ve岩臣、lilins、LittlePineapple宵膨、MatildaJin架谎、冬青、pobusama辟躏、Cherry谷扣、蘿卜、vavd317鸿脓、vivaxy抑钟、萌萌、zhouyao
第 3 章:管理函數(shù)的輸入(Inputs)
在第 2 章的 “函數(shù)輸入” 小節(jié)中野哭,我們聊到了函數(shù)形參(parameters)和實(shí)參(arguments)的基本知識(shí)在塔,實(shí)際上還了解到一些能簡(jiǎn)化其使用方式的語(yǔ)法技巧,比如 ...
操作符和解構(gòu)(destructuring)拨黔。
在那個(gè)討論中蛔溃,我建議盡可能設(shè)計(jì)單一形參的函數(shù)。但實(shí)際上你不能每次都做到,而且也不能每次都掌控你的函數(shù)簽名(譯者注:JS 中贺待,函數(shù)簽名一般包含函數(shù)名和形參等函數(shù)關(guān)鍵信息徽曲,例如 foo(a, b = 1, c)
)。
現(xiàn)在麸塞,我們把注意力放在更復(fù)雜秃臣、強(qiáng)大的模式上,以便討論處在這些場(chǎng)景下的函數(shù)輸入哪工。
立即傳參和稍后傳參
如果一個(gè)函數(shù)接收多個(gè)實(shí)參奥此,你可能會(huì)想先指定部分實(shí)參,余下的稍后再指定雁比。
來(lái)看這個(gè)函數(shù):
function ajax(url,data,callback) {
// ..
}
想象一個(gè)場(chǎng)景稚虎,你要發(fā)起多個(gè)已知 URL 的 API 請(qǐng)求,但這些請(qǐng)求的數(shù)據(jù)和處理響應(yīng)信息的回調(diào)函數(shù)要稍后才能知道偎捎。
當(dāng)然蠢终,你可以等到這些東西都確定后再發(fā)起 ajax(..)
請(qǐng)求,并且到那時(shí)再引用全局 URL 常量茴她。但我們還有另一種選擇寻拂,就是創(chuàng)建一個(gè)已經(jīng)預(yù)設(shè) url
實(shí)參的函數(shù)引用。
我們將創(chuàng)建一個(gè)新函數(shù)丈牢,其內(nèi)部仍然發(fā)起 ajax(..)
請(qǐng)求兜喻,此外在等待接收另外兩個(gè)實(shí)參的同時(shí),我們手動(dòng)將 ajax(..)
第一個(gè)實(shí)參設(shè)置成你關(guān)心的 API 地址赡麦。
function getPerson(data,cb) {
ajax( "http://some.api/person", data, cb );
}
function getOrder(data,cb) {
ajax( "http://some.api/order", data, cb );
}
手動(dòng)指定這些外層函數(shù)當(dāng)然是完全有可能的,但這可能會(huì)變得冗長(zhǎng)乏味帕识,特別是不同的預(yù)設(shè)實(shí)參還會(huì)變化的時(shí)候泛粹,譬如:
function getCurrentUser(cb) {
getPerson( { user: CURRENT_USER_ID }, cb );
}
函數(shù)式編程者習(xí)慣于在重復(fù)做同一種事情的地方找到模式,并試著將這些行為轉(zhuǎn)換為邏輯可重用的實(shí)用函數(shù)肮疗。實(shí)際上晶姊,該行為肯定已是大多數(shù)讀者的本能反應(yīng)了,所以這并非函數(shù)式編程獨(dú)有伪货。但是们衙,對(duì)函數(shù)式編程而言,這個(gè)行為的重要性是毋庸置疑的碱呼。
為了構(gòu)思這個(gè)用于實(shí)參預(yù)設(shè)的實(shí)用函數(shù)蒙挑,我們不僅要著眼于之前提到的手動(dòng)實(shí)現(xiàn)方式,還要在概念上審視一下到底發(fā)生了什么愚臀。
用一句話來(lái)說(shuō)明發(fā)生的事情:getOrder(data,cb)
是 ajax(url,data,cb)
函數(shù)的偏函數(shù)(partially-applied functions)忆蚀。該術(shù)語(yǔ)代表的概念是:在函數(shù)調(diào)用現(xiàn)場(chǎng)(function call-site),將實(shí)參應(yīng)用(apply) 于形參。如你所見(jiàn)馋袜,我們一開(kāi)始僅應(yīng)用了部分實(shí)參 —— 具體是將實(shí)參應(yīng)用到 url
形參 —— 剩下的實(shí)參稍后再應(yīng)用男旗。
關(guān)于該模式更正式的說(shuō)法是:偏函數(shù)嚴(yán)格來(lái)講是一個(gè)減少函數(shù)參數(shù)個(gè)數(shù)(arity)的過(guò)程;這里的參數(shù)個(gè)數(shù)指的是希望傳入的形參的數(shù)量欣鳖。我們通過(guò) getOrder(..)
把原函數(shù) ajax(..)
的參數(shù)個(gè)數(shù)從 3 個(gè)減少到了 2 個(gè)无午。
讓我們定義一個(gè) partial(..)
實(shí)用函數(shù):
function partial(fn,...presetArgs) {
return function partiallyApplied(...laterArgs){
return fn( ...presetArgs, ...laterArgs );
};
}
建議: 只是走馬觀花是不行的。請(qǐng)花些時(shí)間研究一下該實(shí)用函數(shù)中發(fā)生的事情搁骑。請(qǐng)確保你真的理解了访娶。由于在接下來(lái)的文章里,我們將會(huì)一次又一次地提到該模式师痕,所以你最好現(xiàn)在就適應(yīng)它溃睹。
partial(..)
函數(shù)接收 fn
參數(shù),來(lái)表示被我們偏應(yīng)用實(shí)參(partially apply)的函數(shù)胰坟。接著因篇,fn
形參之后,presetArgs
數(shù)組收集了后面?zhèn)魅氲膶?shí)參笔横,保存起來(lái)稍后使用竞滓。
我們創(chuàng)建并 return
了一個(gè)新的內(nèi)部函數(shù)(為了清晰明了,我們把它命名為partiallyApplied(..)
)吹缔,該函數(shù)中商佑,laterArgs
數(shù)組收集了全部實(shí)參。
你注意到在內(nèi)部函數(shù)中的 fn
和 presetArgs
引用了嗎厢塘?他們是怎么如何工作的茶没?在函數(shù) partial(..)
結(jié)束運(yùn)行后,內(nèi)部函數(shù)為何還能訪問(wèn) fn
和 presetArgs
引用晚碾?你答對(duì)了抓半,就是因?yàn)?strong>閉包!內(nèi)部函數(shù) partiallyApplied(..)
封閉(closes over)了 fn
和 presetArgs
變量格嘁,所以無(wú)論該函數(shù)在哪里運(yùn)行笛求,在 partial(..)
函數(shù)運(yùn)行后我們?nèi)匀豢梢栽L問(wèn)這些變量。所以理解閉包是多么的重要糕簿!
當(dāng) partiallyApplied(..)
函數(shù)稍后在某處執(zhí)行時(shí)探入,該函數(shù)使用被閉包作用(closed over)的 fn
引用來(lái)執(zhí)行原函數(shù),首先傳入(被閉包作用的)presetArgs
數(shù)組中所有的偏應(yīng)用(partial application)實(shí)參懂诗,然后再進(jìn)一步傳入 laterArgs
數(shù)組中的實(shí)參蜂嗽。
如果你對(duì)以上感到任何疑惑,請(qǐng)停下來(lái)再看一遍殃恒。相信我徒爹,隨著我們進(jìn)一步深入本文荚醒,你會(huì)欣然接受這個(gè)建議。
提一句隆嗅,對(duì)于這類代碼界阁,函數(shù)式編程者往往喜歡使用更簡(jiǎn)短的 =>
箭頭函數(shù)語(yǔ)法(請(qǐng)看第 2 章的 “語(yǔ)法” 小節(jié)),像這樣:
var partial =
(fn, ...presetArgs) =>
(...laterArgs) =>
fn( ...presetArgs, ...laterArgs );
毫無(wú)疑問(wèn)這更加簡(jiǎn)潔胖喳,甚至代碼稀少泡躯。但我個(gè)人覺(jué)得,無(wú)論我們從數(shù)學(xué)符號(hào)的對(duì)稱性上獲得什么好處丽焊,都會(huì)因函數(shù)變成了匿名函數(shù)而在整體的可讀性上失去更多益處较剃。此外,由于作用域邊界變得模糊技健,我們會(huì)更加難以辯認(rèn)閉包写穴。
不管你喜歡哪種語(yǔ)法實(shí)現(xiàn)方式,現(xiàn)在我們用 partial(..)
實(shí)用函數(shù)來(lái)制造這些之前提及的偏函數(shù):
var getPerson = partial( ajax, "http://some.api/person" );
var getOrder = partial( ajax, "http://some.api/order" );
請(qǐng)暫停并思考一下 getPerson(..)
函數(shù)的外形和內(nèi)在雌贱。它相當(dāng)于下面這樣:
var getPerson = function partiallyApplied(...laterArgs) {
return ajax( "http://some.api/person", ...laterArgs );
};
創(chuàng)建 getOrder(..)
函數(shù)可以依葫蘆畫瓢啊送。但是 getCurrentUser(..)
函數(shù)又如何呢?
// 版本 1
var getCurrentUser = partial(
ajax,
"http://some.api/person",
{ user: CURRENT_USER_ID }
);
// 版本 2
var getCurrentUser = partial( getPerson, { user: CURRENT_USER_ID } );
我們可以(版本 1)直接通過(guò)指定 url
和 data
兩個(gè)實(shí)參來(lái)定義 getCurrentUser(..)
函數(shù)欣孤,也可以(版本 2)將 getCurrentUser(..)
函數(shù)定義成 getPerson(..)
的偏應(yīng)用馋没,該偏應(yīng)用僅指定一個(gè)附加的 data
實(shí)參。
因?yàn)榘姹?2 重用了已經(jīng)定義好的函數(shù)降传,所以它在表達(dá)上更清晰一些篷朵。因此我認(rèn)為它更加貼合函數(shù)式編程精神。
版本 1 和 2 分別相當(dāng)于下面的代碼婆排,我們僅用這些代碼來(lái)確認(rèn)一下對(duì)兩個(gè)函數(shù)版本內(nèi)部運(yùn)行機(jī)制的理解声旺。
// 版本 1
var getCurrentUser = function partiallyApplied(...laterArgs) {
return ajax(
"http://some.api/person",
{ user: CURRENT_USER_ID },
...laterArgs
);
};
// 版本 2
var getCurrentUser = function outerPartiallyApplied(...outerLaterArgs) {
var getPerson = function innerPartiallyApplied(...innerLaterArgs){
return ajax( "http://some.api/person", ...innerLaterArgs );
};
return getPerson( { user: CURRENT_USER_ID }, ...outerLaterArgs );
}
再?gòu)?qiáng)調(diào)一下,為了確保你理解這些代碼段發(fā)生了什么段只,請(qǐng)暫停并重新閱讀一下它們艾少。
注意: 第二個(gè)版本的函數(shù)包含了一個(gè)額外的函數(shù)包裝層。這看起來(lái)有些奇怪而且多余翼悴,但對(duì)于你真正要適應(yīng)的函數(shù)式編程來(lái)說(shuō),這僅僅是它的冰山一角幔妨。隨著本文的繼續(xù)深入鹦赎,我們將會(huì)把許多函數(shù)互相包裝起來(lái)。記住误堡,這就是函數(shù)式編程古话!
我們接著看另外一個(gè)偏應(yīng)用的實(shí)用示例。設(shè)想一個(gè) add(..)
函數(shù)锁施,它接收兩個(gè)實(shí)參陪踩,并取二者之和:
function add(x,y) {
return x + y;
}
現(xiàn)在杖们,想象我們要拿到一個(gè)數(shù)字列表,并且給其中每個(gè)數(shù)字加一個(gè)確定的數(shù)值肩狂。我們將使用 JS 數(shù)組對(duì)象內(nèi)置的 map(..)
實(shí)用函數(shù)摘完。
[1,2,3,4,5].map( function adder(val){
return add( 3, val );
} );
// [4,5,6,7,8]
注意: 如果你沒(méi)見(jiàn)過(guò) map(..)
,別擔(dān)心傻谁,我們會(huì)在本書(shū)后面的部分詳細(xì)介紹它孝治。目前你只需要知道它用來(lái)循環(huán)遍歷(loop over)一個(gè)數(shù)組,在遍歷過(guò)程中調(diào)用函數(shù)產(chǎn)出新值并存到新的數(shù)組中审磁。
因?yàn)?add(..)
函數(shù)簽名不是 map(..)
函數(shù)所預(yù)期的谈飒,所以我們不直接把它傳入 map(..)
函數(shù)里。這樣一來(lái)态蒂,偏應(yīng)用就有了用武之地:我們可以調(diào)整 add(..)
函數(shù)簽名杭措,以符合 map(..)
函數(shù)的預(yù)期。
[1,2,3,4,5].map( partial( add, 3 ) );
// [4,5,6,7,8]
bind(..)
JavaScript 有一個(gè)內(nèi)建的 bind(..)
實(shí)用函數(shù)钾恢,任何函數(shù)都可以使用它手素。該函數(shù)有兩個(gè)功能:預(yù)設(shè) this
關(guān)鍵字的上下文,以及偏應(yīng)用實(shí)參赘那。
我認(rèn)為將這兩個(gè)功能混合進(jìn)一個(gè)實(shí)用函數(shù)是極其糟糕的決定刑桑。有時(shí)你不想關(guān)心 this
的綁定,而只是要偏應(yīng)用實(shí)參募舟。我本人基本上從不會(huì)同時(shí)需要這兩個(gè)功能祠斧。
對(duì)于下面的方案,你通常要傳 null
給用來(lái)綁定 this
的實(shí)參(第一個(gè)實(shí)參)拱礁,而它是一個(gè)可以忽略的占位符琢锋。因此,這個(gè)方案非常糟糕呢灶。
請(qǐng)看:
var getPerson = ajax.bind( null, "http://some.api/person" );
那個(gè) null
只會(huì)給我?guī)?lái)無(wú)盡的煩惱吴超。
將實(shí)參順序顛倒
回想我們之前調(diào)用 Ajax 函數(shù)的方式:ajax( url, data, cb )
。如果要偏應(yīng)用 cb
而稍后再指定 data
和 url
參數(shù)鸯乃,我們應(yīng)該怎么做呢鲸阻?我們可以創(chuàng)建一個(gè)可以顛倒實(shí)參順序的實(shí)用函數(shù),用來(lái)包裝原函數(shù)缨睡。
function reverseArgs(fn) {
return function argsReversed(...args){
return fn( ...args.reverse() );
};
}
// ES6 箭頭函數(shù)形式
var reverseArgs =
fn =>
(...args) =>
fn( ...args.reverse() );
現(xiàn)在可以顛倒 ajax(..)
實(shí)參的順序了鸟悴,接下來(lái),我們不再?gòu)淖筮呴_(kāi)始奖年,而是從右側(cè)開(kāi)始偏應(yīng)用實(shí)參细诸。為了恢復(fù)期望的實(shí)參順序,接著我們又將偏應(yīng)用實(shí)參后的函數(shù)顛倒一下實(shí)參順序:
var cache = {};
var cacheResult = reverseArgs(
partial( reverseArgs( ajax ), function onResult(obj){
cache[obj.id] = obj;
} )
);
// 處理后:
cacheResult( "http://some.api/person", { user: CURRENT_USER_ID } );
好陋守,我們來(lái)定義一個(gè)從右邊開(kāi)始偏應(yīng)用實(shí)參(譯者注:以下簡(jiǎn)稱右偏應(yīng)用實(shí)參)的 partialRight(..)
實(shí)用函數(shù)震贵。我們將運(yùn)用和上面相同的技巧于該函數(shù)中:
function partialRight( fn, ...presetArgs ) {
return reverseArgs(
partial( reverseArgs( fn ), ...presetArgs.reverse() )
);
}
var cacheResult = partialRight( ajax, function onResult(obj){
cache[obj.id] = obj;
});
// 處理后:
cacheResult( "http://some.api/person", { user: CURRENT_USER_ID } );
這個(gè) partialRight(..)
函數(shù)的實(shí)現(xiàn)方案不能保證讓一個(gè)特定的形參接收特定的被偏應(yīng)用的值利赋;它只能確保將被這些值(一個(gè)或幾個(gè))當(dāng)作原函數(shù)最右邊的實(shí)參(一個(gè)或幾個(gè))傳入。
舉個(gè)例子:
function foo(x,y,z) {
var rest = [].slice.call( arguments, 3 );
console.log( x, y, z, rest );
}
var f = partialRight( foo, "z:last" );
f( 1, 2 ); // 1 2 "z:last" []
f( 1 ); // 1 "z:last" undefined []
f( 1, 2, 3 ); // 1 2 3 ["z:last"]
f( 1, 2, 3, 4 ); // 1 2 3 [4,"z:last"]
只有在傳兩個(gè)實(shí)參(匹配到 x
和 y
形參)調(diào)用 f(..)
函數(shù)時(shí)猩系,"z:last"
這個(gè)值才能被賦給函數(shù)的形參 z
媚送。在其他的例子里,不管左邊有多少個(gè)實(shí)參蝙眶,"z:last"
都被傳給最右的實(shí)參季希。
一次傳一個(gè)
我們來(lái)看一個(gè)跟偏應(yīng)用類似的技術(shù),該技術(shù)將一個(gè)期望接收多個(gè)實(shí)參的函數(shù)拆解成連續(xù)的鏈?zhǔn)胶瘮?shù)(chained functions)幽纷,每個(gè)鏈?zhǔn)胶瘮?shù)接收單一實(shí)參(實(shí)參個(gè)數(shù):1)并返回另一個(gè)接收下一個(gè)實(shí)參的函數(shù)式塌。
這就是柯里化(currying)技術(shù)。
首先友浸,想象我們已創(chuàng)建了一個(gè) ajax(..)
的柯里化版本峰尝。我們這樣使用它:
curriedAjax( "http://some.api/person" )
( { user: CURRENT_USER_ID } )
( function foundUser(user){ /* .. */ } );
我們將三次調(diào)用分別拆解開(kāi)來(lái),這也許有助于我們理解整個(gè)過(guò)程:
var personFetcher = curriedAjax( "http://some.api/person" );
var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } );
getCurrentUser( function foundUser(user){ /* .. */ } );
該 curriedAjax(..)
函數(shù)在每次調(diào)用中收恢,一次只接收一個(gè)實(shí)參武学,而不是一次性接收所有實(shí)參(像 ajax(..)
那樣),也不是先傳部分實(shí)參再傳剩余部分實(shí)參(借助 partial(..)
函數(shù))伦意。
柯里化和偏應(yīng)用相似火窒,每個(gè)類似偏應(yīng)用的連續(xù)柯里化調(diào)用都把另一個(gè)實(shí)參應(yīng)用到原函數(shù),一直到所有實(shí)參傳遞完畢驮肉。
不同之處在于熏矿,curriedAjax(..)
函數(shù)會(huì)明確地返回一個(gè)期望只接收下一個(gè)實(shí)參 data
的函數(shù)(我們把它叫做 curriedGetPerson(..)
),而不是那個(gè)能接收所有剩余實(shí)參的函數(shù)(像此前的 getPerson(..)
函數(shù)) 离钝。
如果一個(gè)原函數(shù)期望接收 5 個(gè)實(shí)參票编,這個(gè)函數(shù)的柯里化形式只會(huì)接收第一個(gè)實(shí)參,并且返回一個(gè)用來(lái)接收第二個(gè)參數(shù)的函數(shù)卵渴。而這個(gè)被返回的函數(shù)又只接收第二個(gè)參數(shù)慧域,并且返回一個(gè)接收第三個(gè)參數(shù)的函數(shù)。依此類推浪读。
由此而知昔榴,柯里化將一個(gè)多參數(shù)(higher-arity)函數(shù)拆解為一系列的單元鏈?zhǔn)胶瘮?shù)。
如何定義一個(gè)用來(lái)柯里化的實(shí)用函數(shù)呢碘橘?我們將要用到第 2 章中的一些技巧互订。
function curry(fn,arity = fn.length) {
return (function nextCurried(prevArgs){
return function curried(nextArg){
var args = prevArgs.concat( [nextArg] );
if (args.length >= arity) {
return fn( ...args );
}
else {
return nextCurried( args );
}
};
})( [] );
}
ES6 箭頭函數(shù)版本:
var curry =
(fn, arity = fn.length, nextCurried) =>
(nextCurried = prevArgs =>
nextArg => {
var args = prevArgs.concat( [nextArg] );
if (args.length >= arity) {
return fn( ...args );
}
else {
return nextCurried( args );
}
}
)( [] );
此處的實(shí)現(xiàn)方式是把空數(shù)組 []
當(dāng)作 prevArgs
的初始實(shí)參集合,并且將每次接收到的 nextArg
同 prevArgs
連接成 args
數(shù)組蛹屿。當(dāng) args.length
小于 arity
(原函數(shù) fn(..)
被定義和期望的形參數(shù)量)時(shí),返回另一個(gè) curried(..)
函數(shù)(譯者注:這里指代 nextCurried(..)
返回的函數(shù))用來(lái)接收下一個(gè) nextArg
實(shí)參岩榆,與此同時(shí)將 args
實(shí)參集合作為唯一的 prevArgs
參數(shù)傳入 nextCurried(..)
函數(shù)错负。一旦我們收集了足夠長(zhǎng)度的 args
數(shù)組坟瓢,就用這些實(shí)參觸發(fā)原函數(shù) fn(..)
。
默認(rèn)地犹撒,我們的實(shí)現(xiàn)方案基于下面的條件:在拿到原函數(shù)期望的全部實(shí)參之前折联,我們能夠通過(guò)檢查將要被柯里化的函數(shù)的 length
屬性來(lái)得知柯里化需要迭代多少次。
假如你將該版本的 curry(..)
函數(shù)用在一個(gè) length
屬性不明確的函數(shù)上 —— 函數(shù)的形參聲明包含默認(rèn)形參值识颊、形參解構(gòu)诚镰,或者它是可變參數(shù)函數(shù),用 ...args
當(dāng)形參祥款;參考第 2 章 —— 你將要傳入 arity
參數(shù)(作為 curry(..)
的第二個(gè)形參)來(lái)確保 curry(..)
函數(shù)的正常運(yùn)行清笨。
我們用 curry(..)
函數(shù)來(lái)實(shí)現(xiàn)此前的 ajax(..)
例子:
var curriedAjax = curry( ajax );
var personFetcher = curriedAjax( "http://some.api/person" );
var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } );
getCurrentUser( function foundUser(user){ /* .. */ } );
如上,我們每次函數(shù)調(diào)用都會(huì)新增一個(gè)實(shí)參刃跛,最終給原函數(shù) ajax(..)
使用抠艾,直到收齊三個(gè)實(shí)參并執(zhí)行 ajax(..)
函數(shù)為止。
還記得前面講到為數(shù)值列表的每個(gè)值加 3
的那個(gè)例子嗎桨昙?回顧一下检号,由于柯里化是和偏應(yīng)用相似的,所以我們可以用幾乎相同的方式以柯里化來(lái)完成那個(gè)例子蛙酪。
[1,2,3,4,5].map( curry( add )( 3 ) );
// [4,5,6,7,8]
partial(add,3)
和 curry(add)(3)
兩者有什么不同呢齐苛?為什么你會(huì)選 curry(..)
而不是偏函數(shù)呢?當(dāng)你先得知 add(..)
是將要被調(diào)整的函數(shù)桂塞,但如果這個(gè)時(shí)候并不能確定 3
這個(gè)值凹蜂,柯里化可能會(huì)起作用:
var adder = curry( add );
// later
[1,2,3,4,5].map( adder( 3 ) );
// [4,5,6,7,8]
讓我們來(lái)看看另一個(gè)有關(guān)數(shù)字的例子,這次我們拿一個(gè)列表的數(shù)字做加法:
function sum(...args) {
var sum = 0;
for (let i = 0; i < args.length; i++) {
sum += args[i];
}
return sum;
}
sum( 1, 2, 3, 4, 5 ); // 15
// 好藐俺,我們看看用柯里化怎么做:
// (5 用來(lái)指定需要鏈?zhǔn)秸{(diào)用的次數(shù))
var curriedSum = curry( sum, 5 );
curriedSum( 1 )( 2 )( 3 )( 4 )( 5 ); // 15
這里柯里化的好處是炊甲,每次函數(shù)調(diào)用傳入一個(gè)實(shí)參,并生成另一個(gè)特定性更強(qiáng)的函數(shù)欲芹,之后我們可以在程序中獲取并使用那個(gè)新函數(shù)卿啡。而偏應(yīng)用則是預(yù)先指定所有將被偏應(yīng)用的實(shí)參,產(chǎn)出一個(gè)等待接收剩下所有實(shí)參的函數(shù)菱父。
如果想用偏應(yīng)用來(lái)每次指定一個(gè)形參颈娜,你得在每個(gè)函數(shù)中逐次調(diào)用 partialApply(..)
函數(shù)。而被柯里化的函數(shù)可以自動(dòng)完成這個(gè)工作浙宜,這讓一次單獨(dú)傳遞一個(gè)參數(shù)變得更加符合人機(jī)工程學(xué)官辽。
在 JavaScript 中,柯里化和偏應(yīng)用都使用閉包來(lái)保存實(shí)參粟瞬,直到收齊所有實(shí)參后我們?cè)賵?zhí)行原函數(shù)同仆。
柯里化和偏應(yīng)用有什么用?
無(wú)論是柯里化風(fēng)格(sum(1)(2)(3)
)還是偏應(yīng)用風(fēng)格(partial(sum,1,2)(3)
)裙品,它們的簽名比普通函數(shù)簽名奇怪得多俗批。那么俗或,在適應(yīng)函數(shù)式編程的時(shí)候,我們?yōu)槭裁匆@么做呢岁忘?答案有幾個(gè)方面辛慰。
首先是顯而易見(jiàn)的理由,使用柯里化和偏應(yīng)用可以將指定分離實(shí)參的時(shí)機(jī)和地方獨(dú)立開(kāi)來(lái)(遍及代碼的每一處)干像,而傳統(tǒng)函數(shù)調(diào)用則需要預(yù)先確定所有實(shí)參帅腌。如果你在代碼某一處只獲取了部分實(shí)參,然后在另一處確定另一部分實(shí)參麻汰,這個(gè)時(shí)候柯里化和偏應(yīng)用就能派上用場(chǎng)速客。
另一個(gè)最能體現(xiàn)柯里化應(yīng)用的的是,當(dāng)函數(shù)只有一個(gè)形參時(shí)什乙,我們能夠比較容易地組合它們挽封。因此,如果一個(gè)函數(shù)最終需要三個(gè)實(shí)參臣镣,那么它被柯里化以后會(huì)變成需要三次調(diào)用辅愿,每次調(diào)用需要一個(gè)實(shí)參的函數(shù)。當(dāng)我們組合函數(shù)時(shí)忆某,這種單元函數(shù)的形式會(huì)讓我們處理起來(lái)更簡(jiǎn)單点待。我們將在后面繼續(xù)探討這個(gè)話題。
如何柯里化多個(gè)實(shí)參弃舒?
到目前為止癞埠,我相信我給出的是我們能在 JavaScript 中能得到的,最精髓的柯里化定義和實(shí)現(xiàn)方式聋呢。
具體來(lái)說(shuō)苗踪,如果簡(jiǎn)單看下柯里化在 Haskell 語(yǔ)言中的應(yīng)用,我們會(huì)發(fā)現(xiàn)一個(gè)函數(shù)總是在一次柯里化調(diào)用中接收多個(gè)實(shí)參 —— 而不是接收一個(gè)包含多個(gè)值的元組(tuple削锰,類似我們的數(shù)組)實(shí)參通铲。
在 Haskell 中的示例:
foo 1 2 3
該示例調(diào)用了 foo
函數(shù),并且根據(jù)傳入的三個(gè)值 1
器贩、2
和 3
得到了結(jié)果颅夺。但是在 Haskell 中,函數(shù)會(huì)自動(dòng)被柯里化蛹稍,這意味著我們傳入函數(shù)的值都分別傳入了單獨(dú)的柯里化調(diào)用吧黄。在 JS 中看起來(lái)則會(huì)是這樣:foo(1)(2)(3)
。這和我此前講過(guò)的 curry(..)
風(fēng)格如出一轍唆姐。
注意: 在 Haskell 中拗慨,foo (1,2,3)
不是把三個(gè)值當(dāng)作單獨(dú)的實(shí)參一次性傳入函數(shù),而是把它們包含在一個(gè)元組(類似 JS 數(shù)組)中作為單獨(dú)實(shí)參傳入函數(shù)。為了正常運(yùn)行赵抢,我們需要改變 foo
函數(shù)來(lái)處理作為實(shí)參的元組瘫想。據(jù)我所知,在 Haskell 中我們沒(méi)有辦法在一次函數(shù)調(diào)用中將全部三個(gè)實(shí)參獨(dú)立地傳入昌讲,而需要柯里化調(diào)用每個(gè)函數(shù)。誠(chéng)然减噪,多次調(diào)用對(duì)于 Haskell 開(kāi)發(fā)者來(lái)說(shuō)是透明的短绸,但對(duì) JS 開(kāi)發(fā)者來(lái)說(shuō),這在語(yǔ)法上更加一目了然筹裕。
基于以上原因醋闭,我認(rèn)為此前展示的 curry(..)
函數(shù)是一個(gè)對(duì) Haskell 柯里化的可靠改編,我把它叫做 “嚴(yán)格柯里化”朝卒。
然而证逻,我們需要注意,大多數(shù)流行的 JavaScript 函數(shù)式編程庫(kù)都使用了一種并不嚴(yán)格的柯里化(loose currying)定義抗斤。
具體來(lái)說(shuō)囚企,往往 JS 柯里化實(shí)用函數(shù)會(huì)允許你在每次柯里化調(diào)用中指定多個(gè)實(shí)參∪鹧郏回顧一下之前提到的 sum(..)
示例龙宏,松散柯里化應(yīng)用會(huì)是下面這樣:
var curriedSum = looseCurry( sum, 5 );
curriedSum( 1 )( 2, 3 )( 4, 5 ); // 15
可以看到,語(yǔ)法上我們節(jié)省了()
的使用伤疙,并且把五次函數(shù)調(diào)用減少成三次银酗,間接提高了性能。除此之外徒像,使用 looseCurry(..)
函數(shù)的結(jié)果也和之前更加狹義的 curry(..)
函數(shù)一樣黍特。我猜便利性和性能因素是眾框架允許多實(shí)參柯里化的原因。這看起來(lái)更像是品味問(wèn)題锯蛀。
注意: 松散柯里化允許你傳入超過(guò)形參數(shù)量(arity灭衷,原函數(shù)確認(rèn)或指定的形參數(shù)量)的實(shí)參。如果你將函數(shù)的參數(shù)設(shè)計(jì)成可配的或變化的谬墙,那么松散柯里化將會(huì)有利于你今布。例如,如果你將要柯里化的函數(shù)接收 5 個(gè)實(shí)參拭抬,松散柯里化依然允許傳入超過(guò) 5 個(gè)的實(shí)參(curriedSum(1)(2,3,4)(5,6)
)部默,而嚴(yán)格柯里化就不支持 curriedSum(1)(2)(3)(4)(5)(6)
。
我們可以將之前的柯里化實(shí)現(xiàn)方式調(diào)整一下造虎,使其適應(yīng)這種常見(jiàn)的更松散的定義:
function looseCurry(fn,arity = fn.length) {
return (function nextCurried(prevArgs){
return function curried(...nextArgs){
var args = prevArgs.concat( nextArgs );
if (args.length >= arity) {
return fn( ...args );
}
else {
return nextCurried( args );
}
};
})( [] );
}
現(xiàn)在每個(gè)柯里化調(diào)用可以接收一個(gè)或多個(gè)實(shí)參了(收集在 nextArgs
數(shù)組中)傅蹂。至于這個(gè)實(shí)用函數(shù)的 ES6 箭頭函數(shù)版本,我們就留作一個(gè)小練習(xí),有興趣的讀者可以模仿之前 curry(..)
函數(shù)的來(lái)完成份蝴。
反柯里化
你也會(huì)遇到這種情況:拿到一個(gè)柯里化后的函數(shù)犁功,卻想要它柯里化之前的版本 —— 這本質(zhì)上就是想將類似 f(1)(2)(3)
的函數(shù)變回類似 g(1,2,3)
的函數(shù)。
不出意料的話婚夫,處理這個(gè)需求的標(biāo)準(zhǔn)實(shí)用函數(shù)通常被叫作 uncurry(..)
浸卦。下面是簡(jiǎn)陋的實(shí)現(xiàn)方式:
function uncurry(fn) {
return function uncurried(...args){
var ret = fn;
for (let i = 0; i < args.length; i++) {
ret = ret( args[i] );
}
return ret;
};
}
// ES6 箭頭函數(shù)形式
var uncurry =
fn =>
(...args) => {
var ret = fn;
for (let i = 0; i < args.length; i++) {
ret = ret( args[i] );
}
return ret;
};
警告: 請(qǐng)不要以為 uncurry(curry(f))
和 f
函數(shù)的行為完全一樣。雖然在某些庫(kù)中案糙,反柯里化使函數(shù)變成和原函數(shù)(譯者注:這里的原函數(shù)指柯里化之前的函數(shù))類似的函數(shù)限嫌,但是凡事皆有例外,我們這里就有一個(gè)例外时捌。如果你傳入原函數(shù)期望數(shù)量的實(shí)參怒医,那么在反柯里化后,函數(shù)的行為(大多數(shù)情況下)和原函數(shù)相同奢讨。然而稚叹,如果你少傳了實(shí)參,就會(huì)得到一個(gè)仍然在等待傳入更多實(shí)參的部分柯里化函數(shù)拿诸。我們?cè)谙旅娴拇a中說(shuō)明這個(gè)怪異行為扒袖。
function sum(...args) {
var sum = 0;
for (let i = 0; i < args.length; i++) {
sum += args[i];
}
return sum;
}
var curriedSum = curry( sum, 5 );
var uncurriedSum = uncurry( curriedSum );
curriedSum( 1 )( 2 )( 3 )( 4 )( 5 ); // 15
uncurriedSum( 1, 2, 3, 4, 5 ); // 15
uncurriedSum( 1, 2, 3 )( 4 )( 5 ); // 15
uncurry()
函數(shù)最為常見(jiàn)的作用對(duì)象很可能并不是人為生成的柯里化函數(shù)(例如上文所示),而是某些操作所產(chǎn)生的已經(jīng)被柯里化了的結(jié)果函數(shù)亩码。我們將在本章后面關(guān)于 “無(wú)形參風(fēng)格” 的討論中闡述這種應(yīng)用場(chǎng)景僚稿。
只要一個(gè)實(shí)參
設(shè)想你向一個(gè)實(shí)用函數(shù)傳入一個(gè)函數(shù),而這個(gè)實(shí)用函數(shù)會(huì)把多個(gè)實(shí)參傳入函數(shù)蟀伸,但可能你只希望你的函數(shù)接收單一實(shí)參蚀同。如果你有個(gè)類似我們前面提到被松散柯里化的函數(shù),它能接收多個(gè)實(shí)參啊掏,但你卻想讓它接收單一實(shí)參蠢络。那么這就是我想說(shuō)的情況。
我們可以設(shè)計(jì)一個(gè)簡(jiǎn)單的實(shí)用函數(shù)迟蜜,它包裝一個(gè)函數(shù)調(diào)用刹孔,確保被包裝的函數(shù)只接收一個(gè)實(shí)參。既然實(shí)際上我們是強(qiáng)制把一個(gè)函數(shù)處理成單參數(shù)函數(shù)(unary)娜睛,那我們索性就這樣命名實(shí)用函數(shù):
function unary(fn) {
return function onlyOneArg(arg){
return fn( arg );
};
}
// ES6 箭頭函數(shù)形式
var unary =
fn =>
arg =>
fn( arg );
我們此前已經(jīng)和 map(..)
函數(shù)打過(guò)照面了髓霞。它調(diào)用傳入其中的 mapping 函數(shù)時(shí)會(huì)傳入三個(gè)實(shí)參:value
、index
和 list
畦戒。如果你希望你傳入 map(..)
的 mapping 函數(shù)只接收一個(gè)參數(shù)方库,比如 value
,你可以使用 unary(..)
函數(shù)來(lái)操作:
function unary(fn) {
return function onlyOneArg(arg){
return fn( arg );
};
}
var adder = looseCurry( sum, 2 );
// 出問(wèn)題了:
[1,2,3,4,5].map( adder( 3 ) );
// ["41,2,3,4,5", "61,2,3,4,5", "81,2,3,4,5", "101, ...
// 用 `unary(..)` 修復(fù)后:
[1,2,3,4,5].map( unary( adder( 3 ) ) );
// [4,5,6,7,8]
另一種常用的 unary(..)
函數(shù)調(diào)用示例:
["1","2","3"].map( parseFloat );
// [1,2,3]
["1","2","3"].map( parseInt );
// [1,NaN,NaN]
["1","2","3"].map( unary( parseInt ) );
// [1,2,3]
對(duì)于 parseInt(str,radix)
這個(gè)函數(shù)調(diào)用障斋,如果 map(..)
函數(shù)調(diào)用它時(shí)在它的第二個(gè)實(shí)參位置傳入 index
纵潦,那么毫無(wú)疑問(wèn) parseInt(..)
會(huì)將 index
理解為 radix
參數(shù)徐鹤,這是我們不希望發(fā)生的。而 unary(..)
函數(shù)創(chuàng)建了一個(gè)只接收第一個(gè)傳入實(shí)參邀层,忽略其他實(shí)參的新函數(shù)返敬,這就意味著傳入 index
不再會(huì)被誤解為 radix
參數(shù)。
傳一個(gè)返回一個(gè)
說(shuō)到只傳一個(gè)實(shí)參的函數(shù)寥院,在函數(shù)式編程工具庫(kù)中有另一種通用的基礎(chǔ)函數(shù):該函數(shù)接收一個(gè)實(shí)參劲赠,然后什么都不做,原封不動(dòng)地返回實(shí)參值秸谢。
function identity(v) {
return v;
}
// ES6 箭頭函數(shù)形式
var identity =
v =>
v;
看起來(lái)這個(gè)實(shí)用函數(shù)簡(jiǎn)單到了無(wú)處可用的地步经磅。但即使是簡(jiǎn)單的函數(shù)在函數(shù)式編程的世界里也能發(fā)揮作用。就像演藝圈有句諺語(yǔ):沒(méi)有小角色钮追,只有小演員。
舉個(gè)例子阿迈,想象一下你要用正則表達(dá)式拆分(split up)一個(gè)字符串元媚,但輸出的數(shù)組中可能包含一些空值。我們可以使用 filter(..)
數(shù)組方法(下文會(huì)詳細(xì)說(shuō)到這個(gè)方法)來(lái)篩除空值苗沧,而我們將 identity(..)
函數(shù)作為 filter(..)
的斷言:
var words = " Now is the time for all... ".split( /\s|\b/ );
words;
// ["","Now","is","the","time","for","all","...",""]
words.filter( identity );
// ["Now","is","the","time","for","all","..."]
既然 identity(..)
會(huì)簡(jiǎn)單地返回傳入的值刊棕,而 JS 會(huì)將每個(gè)值強(qiáng)制轉(zhuǎn)換為 true
或 false
,這樣我們就能在最終的數(shù)組里對(duì)每個(gè)值進(jìn)行保存或排除待逞。
小貼士: 像這個(gè)例子一樣甥角,另外一個(gè)能被用作斷言的單實(shí)參函數(shù)是 JS 自有的 Boolean(..)
方法,該方法會(huì)強(qiáng)制把傳入值轉(zhuǎn)為 true
或 false
识樱。
另一個(gè)使用 identity(..)
的示例就是將其作為替代一個(gè)轉(zhuǎn)換函數(shù)(譯者注:transformation嗤无,這里指的是對(duì)傳入值進(jìn)行修改或調(diào)整,返回新值的函數(shù))的默認(rèn)函數(shù):
function output(msg,formatFn = identity) {
msg = formatFn( msg );
console.log( msg );
}
function upper(txt) {
return txt.toUpperCase();
}
output( "Hello World", upper ); // HELLO WORLD
output( "Hello World" ); // Hello World
如果不給 output(..)
函數(shù)的 formatFn
參數(shù)設(shè)置默認(rèn)值怜庸,我們可以叫出老朋友 partialRight(..)
函數(shù):
var specialOutput = partialRight( output, upper );
var simpleOutput = partialRight( output, identity );
specialOutput( "Hello World" ); // HELLO WORLD
simpleOutput( "Hello World" ); // Hello World
你也可能會(huì)看到 identity(..)
被當(dāng)作 map(..)
函數(shù)調(diào)用的默認(rèn)轉(zhuǎn)換函數(shù)当犯,或者作為某個(gè)函數(shù)數(shù)組的 reduce(..)
函數(shù)的初始值。我們將會(huì)在第 8 章中提到這兩個(gè)實(shí)用函數(shù)割疾。
恒定參數(shù)
Certain API 禁止直接給方法傳值嚎卫,而要求我們傳入一個(gè)函數(shù),就算這個(gè)函數(shù)只是返回一個(gè)值宏榕。JS Promise 中的 then(..)
方法就是一個(gè) Certain API拓诸。很多人聲稱 ES6 箭頭函數(shù)可以當(dāng)作這個(gè)問(wèn)題的 “解決方案”。但我這有一個(gè)函數(shù)式編程實(shí)用函數(shù)可以完美勝任該任務(wù):
function constant(v) {
return function value(){
return v;
};
}
// or the ES6 => form
var constant =
v =>
() =>
v;
這個(gè)微小而簡(jiǎn)潔的實(shí)用函數(shù)可以解決我們關(guān)于 then(..)
的煩惱:
p1.then( foo ).then( () => p2 ).then( bar );
// 對(duì)比:
p1.then( foo ).then( constant( p2 ) ).then( bar );
警告: 盡管使用 () => p2
箭頭函數(shù)的版本比使用 constant(p2)
的版本更簡(jiǎn)短麻昼,但我建議你忍住別用前者奠支。該箭頭函數(shù)返回了一個(gè)來(lái)自外作用域的值,這和 函數(shù)式編程的理念有些矛盾抚芦。我們將會(huì)在后面第 5 章的 “減少副作用” 小節(jié)中提到這種行為帶來(lái)的陷阱胚宦。
擴(kuò)展在參數(shù)中的妙用
在第 2 章中,我們簡(jiǎn)要地講到了形參數(shù)組解構(gòu)∈嗳埃回顧一下該示例:
function foo( [x,y,...args] ) {
// ..
}
foo( [1,2,3] );
在 foo(..)
函數(shù)的形參列表中井联,我們期望接收單一數(shù)組實(shí)參,我們要把這個(gè)數(shù)組拆解 —— 或者更貼切地說(shuō)您旁,擴(kuò)展(spread out)—— 成獨(dú)立的實(shí)參 x
和 y
烙常。除了頭兩個(gè)位置以外的參數(shù)值我們都會(huì)通過(guò) ...
操作將它們收集在 args
數(shù)組中。
當(dāng)函數(shù)必須接收一個(gè)數(shù)組鹤盒,而你卻想把數(shù)組內(nèi)容當(dāng)成單獨(dú)形參來(lái)處理的時(shí)候蚕脏,這個(gè)技巧十分有用。
然而侦锯,有的時(shí)候驼鞭,你無(wú)法改變?cè)瘮?shù)的定義,但想使用形參數(shù)組解構(gòu)尺碰。舉個(gè)例子挣棕,請(qǐng)思考下面的函數(shù):
function foo(x,y) {
console.log( x + y );
}
function bar(fn) {
fn( [ 3, 9 ] );
}
bar( foo ); // 失敗
你注意到為什么 bar(foo)
函數(shù)失敗了嗎?
我們將 [3,9]
數(shù)組作為單一值傳入 fn(..)
函數(shù)亲桥,但 foo(..)
期望接收單獨(dú)的 x
和 y
形參洛心。如果我們可以把 foo(..)
的函數(shù)聲明改變成 function foo([x,y]) { ..
那就好辦了√馀瘢或者词身,我們可以改變 bar(..)
函數(shù)的行為,把調(diào)用改成 fn(...[3,9])
番枚,這樣就能將 3
和 9
分別傳入 foo(..)
函數(shù)了法严。
假設(shè)有兩個(gè)在此方法上互不兼容的函數(shù),而且由于各種原因你無(wú)法改變它們的聲明和定義葫笼。那么你該如何一并使用它們呢渐夸?
為了調(diào)整一個(gè)函數(shù),讓它能把接收的單一數(shù)組擴(kuò)展成各自獨(dú)立的實(shí)參渔欢,我們可以定義一個(gè)輔助函數(shù):
function spreadArgs(fn) {
return function spreadFn(argsArr) {
return fn( ...argsArr );
};
}
// ES6 箭頭函數(shù)的形式:
var spreadArgs =
fn =>
argsArr =>
fn( ...argsArr );
注意: 我把這個(gè)輔助函數(shù)叫做 spreadArgs(..)
墓塌,但一些庫(kù),比如 Ramda奥额,經(jīng)常把它叫做 apply(..)
苫幢。
現(xiàn)在我們可以使用 spreadArgs(..)
來(lái)調(diào)整 foo(..)
函數(shù),使其作為一個(gè)合適的輸入?yún)?shù)并正常地工作:
bar( spreadArgs( foo ) ); // 12
相信我垫挨,雖然我不能講清楚這些問(wèn)題出現(xiàn)的原因韩肝,但它們一定會(huì)出現(xiàn)的。本質(zhì)上九榔,spreadArgs(..)
函數(shù)使我們能夠定義一個(gè)借助數(shù)組 return
多個(gè)值的函數(shù)惭墓,不過(guò),它讓這些值仍然能分別作為其他函數(shù)的輸入?yún)?shù)來(lái)處理趁尼。
一個(gè)函數(shù)的輸出作為另外一個(gè)函數(shù)的輸入被稱作組合(composition),我們將在第四章詳細(xì)討論這個(gè)話題催蝗。
盡管我們?cè)谡務(wù)?spreadArgs(..)
實(shí)用函數(shù),但我們也可以定義一下實(shí)現(xiàn)相反功能的實(shí)用函數(shù):
function gatherArgs(fn) {
return function gatheredFn(...argsArr) {
return fn( argsArr );
};
}
// ES6 箭頭函數(shù)形式
var gatherArgs =
fn =>
(...argsArr) =>
fn( argsArr );
注意: 在 Ramda 中育特,該實(shí)用函數(shù)被稱作 unapply(..)
丙号,是與 apply(..)
功能相反的函數(shù)。我認(rèn)為術(shù)語(yǔ) “擴(kuò)展(spread)” 和 “聚集(gather)” 可以把這兩個(gè)函數(shù)發(fā)生的事情解釋得更好一些缰冤。
因?yàn)橛袝r(shí)我們可能要調(diào)整一個(gè)函數(shù)犬缨,解構(gòu)其數(shù)組形參,使其成為另一個(gè)分別接收單獨(dú)實(shí)參的函數(shù)棉浸,所以我們可以通過(guò)使用 gatherArgs(..)
實(shí)用函數(shù)來(lái)將單獨(dú)的實(shí)參聚集到一個(gè)數(shù)組中怀薛。我們將在第 8 章中細(xì)說(shuō) reduce(..)
函數(shù),這里我們簡(jiǎn)要說(shuō)一下:它重復(fù)調(diào)用傳入的 reducer 函數(shù)迷郑,其中 reducer 函數(shù)有兩個(gè)形參枝恋,現(xiàn)在我們可以將這兩個(gè)形參聚集起來(lái):
function combineFirstTwo([ v1, v2 ]) {
return v1 + v2;
}
[1,2,3,4,5].reduce( gatherArgs( combineFirstTwo ) );
// 15
參數(shù)順序的那些事兒
對(duì)于多形參函數(shù)的柯里化和偏應(yīng)用,我們不得不通過(guò)許多令人懊惱的技巧來(lái)修正這些形參的順序三热。有時(shí)我們把一個(gè)函數(shù)的形參順序定義成柯里化需求的形參順序,但這種順序沒(méi)有兼容性三幻,我們不得不絞盡腦汁來(lái)重新調(diào)整它就漾。
讓人沮喪的可不僅是我們需要使用實(shí)用函數(shù)來(lái)委曲求全,在此之外念搬,這種做法還會(huì)導(dǎo)致我們的代碼被無(wú)關(guān)代碼混淆抑堡。這種東西就像碎紙片,這一片那一片的朗徊,而不是一整個(gè)突出問(wèn)題首妖,但這些問(wèn)題的細(xì)碎絲毫不會(huì)減少它們帶來(lái)的苦惱。
難道就沒(méi)有能讓我們從修正參數(shù)順序這件事里解脫出來(lái)的方法嗎R摇有缆?
在第 2 章里,我們講到了命名實(shí)參(named-argument)解構(gòu)模式温亲∨锉冢回顧一下:
function foo( {x,y} = {} ) {
console.log( x, y );
}
foo( {
y: 3
} ); // undefined 3
我們將 foo(..)
函數(shù)的第一個(gè)形參 —— 它被期望是一個(gè)對(duì)象 —— 解構(gòu)成單獨(dú)的形參 x
和 y
。接著在調(diào)用時(shí)傳入一個(gè)對(duì)象實(shí)參栈虚,并且提供函數(shù)期望的屬性袖外,這樣就可以把 “命名實(shí)參” 映射到相應(yīng)形參上。
命名實(shí)參主要的好處就是不用再糾結(jié)實(shí)參傳入的順序魂务,因此提高了可讀性曼验。我們可以發(fā)掘一下看看是否能設(shè)計(jì)一個(gè)等效的實(shí)用函數(shù)來(lái)處理對(duì)象屬性泌射,以此提高柯里化和偏應(yīng)用的可讀性:
function partialProps(fn,presetArgsObj) {
return function partiallyApplied(laterArgsObj){
return fn( Object.assign( {}, presetArgsObj, laterArgsObj ) );
};
}
function curryProps(fn,arity = 1) {
return (function nextCurried(prevArgsObj){
return function curried(nextArgObj = {}){
var [key] = Object.keys( nextArgObj );
var allArgsObj = Object.assign( {}, prevArgsObj, { [key]: nextArgObj[key] } );
if (Object.keys( allArgsObj ).length >= arity) {
return fn( allArgsObj );
}
else {
return nextCurried( allArgsObj );
}
};
})( {} );
}
我們甚至不需要設(shè)計(jì)一個(gè) partialPropsRight(..)
函數(shù)了,因?yàn)槲覀兏静恍枰紤]屬性的映射順序鬓照,通過(guò)命名來(lái)映射形參完全解決了我們有關(guān)于順序的煩惱熔酷!
我們這樣使用這些使用函數(shù):
function foo({ x, y, z } = {}) {
console.log( `x:${x} y:${y} z:${z}` );
}
var f1 = curryProps( foo, 3 );
var f2 = partialProps( foo, { y: 2 } );
f1( {y: 2} )( {x: 1} )( {z: 3} );
// x:1 y:2 z:3
f2( { z: 3, x: 1 } );
// x:1 y:2 z:3
我們不用再為參數(shù)順序而煩惱了!現(xiàn)在颖杏,我們可以指定我們想傳入的實(shí)參纯陨,而不用管它們的順序如何。再也不需要類似 reverseArgs(..)
的函數(shù)或其它妥協(xié)了留储。贊翼抠!
屬性擴(kuò)展
不幸的是,只有在我們可以掌控 foo(..)
的函數(shù)簽名获讳,并且可以定義該函數(shù)的行為阴颖,使其解構(gòu)第一個(gè)參數(shù)的時(shí)候,以上技術(shù)才能起作用丐膝。如果一個(gè)函數(shù)量愧,其形參是各自獨(dú)立的(沒(méi)有經(jīng)過(guò)形參解構(gòu)),而且不能改變它的函數(shù)簽名帅矗,那我們應(yīng)該如何運(yùn)用這個(gè)技術(shù)呢偎肃?
function bar(x,y,z) {
console.log( `x:${x} y:${y} z:${z}` );
}
就像之前的 spreadArgs(..)
實(shí)用函數(shù)一樣,我們也可以定義一個(gè) spreadArgProps(..)
輔助函數(shù)浑此,它接收對(duì)象實(shí)參的 key: value
鍵值對(duì)累颂,并將其 “擴(kuò)展” 成獨(dú)立實(shí)參。
不過(guò)凛俱,我們需要注意某些異常的地方紊馏。我們使用 spreadArgs(..)
函數(shù)處理數(shù)組實(shí)參時(shí),參數(shù)的順序是明確的蒲犬。然而朱监,對(duì)象屬性的順序是不太明確且不可靠的。取決于不同對(duì)象的創(chuàng)建方式和屬性設(shè)置方式原叮,我們無(wú)法完全確認(rèn)對(duì)象會(huì)產(chǎn)生什么順序的屬性枚舉赫编。
針對(duì)這個(gè)問(wèn)題,我們定義的實(shí)用函數(shù)需要讓你能夠指定函數(shù)期望的實(shí)參順序(比如屬性枚舉的順序)奋隶。我們可以傳入一個(gè)類似 ["x","y","z"]
的數(shù)組沛慢,通知實(shí)用函數(shù)基于該數(shù)組的順序來(lái)獲取對(duì)象實(shí)參的屬性值。
這著實(shí)不錯(cuò)达布,但還是有點(diǎn)瑕疵团甲,就算是最簡(jiǎn)單的函數(shù),我們也免不了為其增添一個(gè)由屬性名構(gòu)成的數(shù)組黍聂。難道我們就沒(méi)有一種可以探知函數(shù)形參順序的技巧嗎躺苦?哪怕給一個(gè)普通而簡(jiǎn)單的例子身腻?還真有!
JavaScript 的函數(shù)對(duì)象上有一個(gè) .toString()
方法匹厘,它返回函數(shù)代碼的字符串形式嘀趟,其中包括函數(shù)聲明的簽名。先忽略其正則表達(dá)式分析技巧愈诚,我們可以通過(guò)解析函數(shù)字符串來(lái)獲取每個(gè)單獨(dú)的命名形參她按。雖然這段代碼看起來(lái)有些粗暴,但它足以滿足我們的需求:
function spreadArgProps(
fn,
propOrder =
fn.toString()
.replace( /^(?:(?:function.*\(([^]*?)\))|(?:([^\(\)]+?)\s*=>)|(?:\(([^]*?)\)\s*=>))[^]+$/, "$1$2$3" )
.split( /\s*,\s*/ )
.map( v => v.replace( /[=\s].*$/, "" ) )
) {
return function spreadFn(argsObj) {
return fn( ...propOrder.map( k => argsObj[k] ) );
};
}
注意: 該實(shí)用函數(shù)的參數(shù)解析邏輯并非無(wú)懈可擊炕柔,使用正則來(lái)解析代碼這個(gè)前提就已經(jīng)很不靠譜了酌泰!但處理一般情況是我們的唯一目標(biāo),從這點(diǎn)來(lái)看這個(gè)實(shí)用函數(shù)還是恰到好處的匕累。我們需要的只是對(duì)簡(jiǎn)單形參(包括帶默認(rèn)值的形參)函數(shù)的形參順序做一個(gè)恰當(dāng)?shù)哪J(rèn)檢測(cè)陵刹。例如,我們的實(shí)用函數(shù)不需要把復(fù)雜的解構(gòu)形參給解析出來(lái)欢嘿,因?yàn)闊o(wú)論如何我們不太可能對(duì)擁有這種復(fù)雜形參的函數(shù)使用 spreadArgProps()
函數(shù)衰琐。因此該邏輯能搞定 80% 的需求,它允許我們?cè)谄渌荒苷_解析復(fù)雜函數(shù)簽名的情況下覆蓋 propOrder
數(shù)組形參炼蹦。這是本書(shū)盡可能尋找的一種實(shí)用性平衡羡宙。
讓我們看看 spreadArgProps(..)
實(shí)用函數(shù)是怎么用的:
function bar(x,y,z) {
console.log( `x:${x} y:${y} z:${z}` );
}
var f3 = curryProps( spreadArgProps( bar ), 3 );
var f4 = partialProps( spreadArgProps( bar ), { y: 2 } );
f3( {y: 2} )( {x: 1} )( {z: 3} );
// x:1 y:2 z:3
f4( { z: 3, x: 1 } );
// x:1 y:2 z:3
提個(gè)醒:本文中呈現(xiàn)的對(duì)象形參(object parameters)和命名實(shí)參(named arguments)模式,通過(guò)減少由調(diào)整實(shí)參順序帶來(lái)的干擾掐隐,明顯地提高了代碼的可讀性狗热,不過(guò)據(jù)我所知,沒(méi)有哪個(gè)主流的函數(shù)式編程庫(kù)使用該方案瑟枫。所以你會(huì)看到該做法與大多數(shù) JavaScript 函數(shù)式編程很不一樣.
此外斗搞,使用在這種風(fēng)格下定義的函數(shù)要求你知道每個(gè)實(shí)參的名字指攒。你必須記卓睹睢:“這個(gè)函數(shù)形參叫作 ‘fn’ ”,而不是只記得:“噢允悦,把這個(gè)函數(shù)作為第一個(gè)實(shí)參傳進(jìn)去”膝擂。
請(qǐng)小心地權(quán)衡它們。
無(wú)形參風(fēng)格
在函數(shù)式編程的世界中隙弛,有一種流行的代碼風(fēng)格架馋,其目的是通過(guò)移除不必要的形參-實(shí)參映射來(lái)減少視覺(jué)上的干擾。這種風(fēng)格的正式名稱為 “隱性編程(tacit programming)”全闷,一般則稱作 “無(wú)形參(point-free)” 風(fēng)格叉寂。術(shù)語(yǔ) “point” 在這里指的是函數(shù)形參。
警告: 且慢总珠,先說(shuō)明我們這次的討論是一個(gè)有邊界的提議屏鳍,我不建議你在函數(shù)式編程的代碼里不惜代價(jià)地濫用無(wú)形參風(fēng)格勘纯。該技術(shù)是用于在適當(dāng)情況下提升可讀性。但你完全可能像濫用軟件開(kāi)發(fā)里大多數(shù)東西一樣濫用它钓瞭。如果你由于必須遷移到無(wú)參數(shù)風(fēng)格而讓代碼難以理解驳遵,請(qǐng)打住。你不會(huì)因此獲得小紅花山涡,因?yàn)槟阌每此坡斆鞯逎y懂的方式抹除形參這個(gè)點(diǎn)的同時(shí)堤结,還抹除了代碼的重點(diǎn)。
我們從一個(gè)簡(jiǎn)單的例子開(kāi)始:
function double(x) {
return x * 2;
}
[1,2,3,4,5].map( function mapper(v){
return double( v );
} );
// [2,4,6,8,10]
可以看到 mapper(..)
函數(shù)和 double(..)
函數(shù)有相同(或相互兼容)的函數(shù)簽名鸭丛。形參(也就是所謂的 “point“)v
可以直接映射到 double(..)
函數(shù)調(diào)用里相應(yīng)的實(shí)參上竞穷。這樣,mapper(..)
函數(shù)包裝層是非必需的系吩。我們可以將其簡(jiǎn)化為無(wú)形參風(fēng)格:
function double(x) {
return x * 2;
}
[1,2,3,4,5].map( double );
// [2,4,6,8,10]
回顧之前的一個(gè)例子:
["1","2","3"].map( function mapper(v){
return parseInt( v );
} );
// [1,2,3]
該例中来庭,mapper(..)
實(shí)際上起著重要作用,它排除了 map(..)
函數(shù)傳入的 index
實(shí)參穿挨,因?yàn)槿绻贿@么做的話月弛,parseInt(..)
函數(shù)會(huì)錯(cuò)把 index
當(dāng)作 radix
來(lái)進(jìn)行整數(shù)解析。該例子中我們可以借助 unary(..)
函數(shù):
["1","2","3"].map( unary( parseInt ) );
// [1,2,3]
使用無(wú)形參風(fēng)格的關(guān)鍵科盛,是找到你代碼中帽衙,有哪些地方的函數(shù)直接將其形參作為內(nèi)部函數(shù)調(diào)用的實(shí)參。以上提到的兩個(gè)例子中贞绵,mapper(..)
函數(shù)拿到形參 v
單獨(dú)傳入了另一個(gè)函數(shù)調(diào)用厉萝。我們可以借助 unary(..)
函數(shù)將提取形參的邏輯層替換成無(wú)參數(shù)形式表達(dá)式。
警告: 你可能跟我一樣榨崩,已經(jīng)嘗試著使用 map(partialRight(parseInt,10))
來(lái)將 10
右偏應(yīng)用為 parseInt(..)
的 radix
實(shí)參谴垫。然而,就像我們之前看到的那樣母蛛,partialRight(..)
僅僅保證將 10
當(dāng)作最后一個(gè)實(shí)參傳入原函數(shù)翩剪,而不是將其指定為第二個(gè)實(shí)參。因?yàn)?map(..)
函數(shù)本身會(huì)將 3 個(gè)實(shí)參(value
彩郊、index
和 arr
)傳入它的映射函數(shù)前弯,所以 10
就會(huì)被當(dāng)成第四個(gè)實(shí)參傳入 parseInt(..)
函數(shù),而這個(gè)函數(shù)只會(huì)對(duì)頭兩個(gè)實(shí)參作出反應(yīng)秫逝。
來(lái)看另一個(gè)例子:
// 將 `console.log` 當(dāng)成一個(gè)函數(shù)使用
// 便于避免潛在的綁定問(wèn)題
function output(txt) {
console.log( txt );
}
function printIf( predicate, msg ) {
if (predicate( msg )) {
output( msg );
}
}
function isShortEnough(str) {
return str.length <= 5;
}
var msg1 = "Hello";
var msg2 = msg1 + " World";
printIf( isShortEnough, msg1 ); // Hello
printIf( isShortEnough, msg2 );
現(xiàn)在恕出,我們要求當(dāng)信息足夠長(zhǎng)時(shí),將它打印出來(lái)违帆,換而言之浙巫,我們需要一個(gè) !isShortEnough(..)
斷言。你可能會(huì)首先想到:
function isLongEnough(str) {
return !isShortEnough( str );
}
printIf( isLongEnough, msg1 );
printIf( isLongEnough, msg2 ); // Hello World
這太簡(jiǎn)單了...但現(xiàn)在我們的重點(diǎn)來(lái)了刷后!你看到了 str
形參是如何傳遞的嗎的畴?我們能否不通過(guò)重新實(shí)現(xiàn) str.length
的檢查邏輯廉油,而重構(gòu)代碼并使其變成無(wú)形參風(fēng)格呢?
我們定義一個(gè) not(..)
取反輔助函數(shù)(在函數(shù)式編程庫(kù)中又被稱作 complement(..)
):
function not(predicate) {
return function negated(...args){
return !predicate( ...args );
};
}
// ES6 箭頭函數(shù)形式
var not =
predicate =>
(...args) =>
!predicate( ...args );
接著苗傅,我們使用 not(..)
函數(shù)來(lái)定義無(wú)形參的 isLongEnough(..)
函數(shù):
var isLongEnough = not( isShortEnough );
printIf( isLongEnough, msg2 ); // Hello World
目前為止已經(jīng)不錯(cuò)了抒线,但還能更進(jìn)一步。我們實(shí)際上可以將 printIf(..)
函數(shù)本身重構(gòu)成無(wú)形參風(fēng)格渣慕。
我們可以用 when(..)
實(shí)用函數(shù)來(lái)表示 if
條件句:
function when(predicate,fn) {
return function conditional(...args){
if (predicate( ...args )) {
return fn( ...args );
}
};
}
// ES6 箭頭函數(shù)形式
var when =
(predicate,fn) =>
(...args) =>
predicate( ...args ) ? fn( ...args ) : undefined;
我們把本章前面講到的另一些輔助函數(shù)和 when(..)
函數(shù)結(jié)合起來(lái)搞定無(wú)形參風(fēng)格的 printIf(..)
函數(shù):
var printIf = uncurry( rightPartial( when, output ) );
我們是這么做的:將 output
方法右偏應(yīng)用為 when(..)
函數(shù)的第二個(gè)(fn
形參)實(shí)參嘶炭,這樣我們得到了一個(gè)仍然期望接收第一個(gè)實(shí)參(predicate
形參)的函數(shù)。當(dāng)該函數(shù)被調(diào)用時(shí)逊桦,會(huì)產(chǎn)生另一個(gè)期望接收(譯者注:需要被打印的)信息字符串的函數(shù)眨猎,看起來(lái)就是這樣:fn(predicate)(str)
。
多個(gè)(兩個(gè))鏈?zhǔn)胶瘮?shù)的調(diào)用看起來(lái)很挫强经,就像被柯里化的函數(shù)睡陪。于是我們用 uncurry(..)
函數(shù)處理它,得到一個(gè)期望接收 str
和 predicate
兩個(gè)實(shí)參的函數(shù)匿情,這樣該函數(shù)的簽名就和 printIf(predicate,str)
原函數(shù)一樣了兰迫。
我們把整個(gè)例子復(fù)盤一下(假設(shè)我們本章已經(jīng)講解的實(shí)用函數(shù)都在這里了):
function output(msg) {
console.log( msg );
}
function isShortEnough(str) {
return str.length <= 5;
}
var isLongEnough = not( isShortEnough );
var printIf = uncurry( partialRight( when, output ) );
var msg1 = "Hello";
var msg2 = msg1 + " World";
printIf( isShortEnough, msg1 ); // Hello
printIf( isShortEnough, msg2 );
printIf( isLongEnough, msg1 );
printIf( isLongEnough, msg2 ); // Hello World
但愿無(wú)形參風(fēng)格編程的函數(shù)式編程實(shí)踐逐漸變得更有意義。你仍然可以通過(guò)大量實(shí)踐來(lái)訓(xùn)練自己炬称,讓自己接受這種風(fēng)格汁果。再次提醒,請(qǐng)三思而后行玲躯,掂量一下是否值得使用無(wú)形參風(fēng)格編程据德,以及使用到什么程度會(huì)益于提高代碼的可讀性。
有形參還是無(wú)形參跷车,你怎么選棘利?
注意: 還有什么無(wú)形參風(fēng)格編程的實(shí)踐呢?我們將在第 4 章的 “回顧形參” 小節(jié)里朽缴,站在新學(xué)習(xí)的組合函數(shù)知識(shí)之上來(lái)回顧這個(gè)技術(shù)善玫。
總結(jié)
偏應(yīng)用是用來(lái)減少函數(shù)的參數(shù)數(shù)量 —— 一個(gè)函數(shù)期望接收的實(shí)參數(shù)量 —— 的技術(shù),它減少參數(shù)數(shù)量的方式是創(chuàng)建一個(gè)預(yù)設(shè)了部分實(shí)參的新函數(shù)不铆。
柯里化是偏應(yīng)用的一種特殊形式蝌焚,其參數(shù)數(shù)量降低為 1裹唆,這種形式包含一串連續(xù)的鏈?zhǔn)胶瘮?shù)調(diào)用誓斥,每個(gè)調(diào)用接收一個(gè)實(shí)參。當(dāng)這些鏈?zhǔn)秸{(diào)用指定了所有實(shí)參時(shí)许帐,原函數(shù)就會(huì)拿到收集好的實(shí)參并執(zhí)行劳坑。你同樣可以將柯里化還原。
其它類似 unary(..)
成畦、identity(..)
以及 constant(..)
的重要函數(shù)操作距芬,是函數(shù)式編程基礎(chǔ)工具庫(kù)的一部分涝开。
無(wú)形參是一種書(shū)寫代碼的風(fēng)格,這種風(fēng)格移除了非必需的形參映射實(shí)參邏輯框仔,其目的在于提高代碼的可讀性和可理解性舀武。
** 【上一章】翻譯連載 |《JavaScript 輕量級(jí)函數(shù)式編程》- 第 2 章:函數(shù)基礎(chǔ) **