翻譯連載 |《JavaScript 輕量級(jí)函數(shù)式編程》- 第3章:管理函數(shù)的輸入

關(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ì)(排名不分先后):阿希报咳、bluekenbrucecham挖藏、cfanlife暑刃、dailkyoko-df膜眠、l3ve岩臣、lilinsLittlePineapple宵膨、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ù)中的 fnpresetArgs 引用了嗎厢塘?他們是怎么如何工作的茶没?在函數(shù) partial(..) 結(jié)束運(yùn)行后,內(nèi)部函數(shù)為何還能訪問(wèn) fnpresetArgs 引用晚碾?你答對(duì)了抓半,就是因?yàn)?strong>閉包!內(nèi)部函數(shù) partiallyApplied(..) 封閉(closes over)了 fnpresetArgs 變量格嘁,所以無(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ò)指定 urldata 兩個(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 而稍后再指定 dataurl 參數(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í)參(匹配到 xy 形參)調(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í)參集合,并且將每次接收到的 nextArgprevArgs 連接成 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器贩、23 得到了結(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í)參:valueindexlist畦戒。如果你希望你傳入 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)換為 truefalse,這樣我們就能在最終的數(shù)組里對(duì)每個(gè)值進(jìn)行保存或排除待逞。

小貼士: 像這個(gè)例子一樣甥角,另外一個(gè)能被用作斷言的單實(shí)參函數(shù)是 JS 自有的 Boolean(..) 方法,該方法會(huì)強(qiáng)制把傳入值轉(zhuǎn)為 truefalse识樱。

另一個(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í)參 xy烙常。除了頭兩個(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ú)的 xy 形參洛心。如果我們可以把 foo(..) 的函數(shù)聲明改變成 function foo([x,y]) { .. 那就好辦了√馀瘢或者词身,我們可以改變 bar(..) 函數(shù)的行為,把調(diào)用改成 fn(...[3,9])番枚,這樣就能將 39 分別傳入 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ú)的形參 xy。接著在調(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彩郊、indexarr)傳入它的映射函數(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è)期望接收 strpredicate 兩個(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ǔ) **

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市离斩,隨后出現(xiàn)的幾起案子银舱,更是在濱河造成了極大的恐慌,老刑警劉巖跛梗,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寻馏,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡核偿,警方通過(guò)查閱死者的電腦和手機(jī)诚欠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)漾岳,“玉大人轰绵,你說(shuō)我怎么就攤上這事∧峋#” “怎么了藏澳?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)耀找。 經(jīng)常有香客問(wèn)我翔悠,道長(zhǎng),這世上最難降的妖魔是什么野芒? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任蓄愁,我火速辦了婚禮,結(jié)果婚禮上狞悲,老公的妹妹穿的比我還像新娘撮抓。我一直安慰自己,他們只是感情好摇锋,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布丹拯。 她就那樣靜靜地躺著,像睡著了一般荸恕。 火紅的嫁衣襯著肌膚如雪乖酬。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天融求,我揣著相機(jī)與錄音咬像,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛县昂,可吹牛的內(nèi)容都是我干的肮柜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼倒彰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼审洞!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起待讳,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤预明,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后耙箍,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體撰糠,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年辩昆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了阅酪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡汁针,死狀恐怖术辐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情施无,我是刑警寧澤辉词,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站猾骡,受9級(jí)特大地震影響瑞躺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜兴想,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一幢哨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嫂便,春花似錦捞镰、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至厂画,卻和暖如春凸丸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背木羹。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工甲雅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坑填。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓抛人,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親脐瑰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子妖枚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容