成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

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

xiaowugui666 / 607人閱讀

摘要:但是,對函數(shù)式編程而言,這個(gè)行為的重要性是毋庸置疑的。關(guān)于該模式更正式的說法是偏函數(shù)嚴(yán)格來講是一個(gè)減少函數(shù)參數(shù)個(gè)數(shù)的過程這里的參數(shù)個(gè)數(shù)指的是希望傳入的形參的數(shù)量。

原文地址:Functional-Light-JS

原文作者:Kyle Simpson-《You-Dont-Know-JS》作者

關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅(jiān)實(shí)的梁柱;分享,是 CSS 里最閃耀的一瞥;總結(jié),是 JavaScript 中最嚴(yán)謹(jǐn)?shù)倪壿?。?jīng)過捶打磨練,成就了本書的中文版。本書包含了函數(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í)際上還了解到一些能簡化其使用方式的語法技巧,比如 ... 操作符和解構(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)大的模式上,以便討論處在這些場景下的函數(shù)輸入。

立即傳參和稍后傳參

如果一個(gè)函數(shù)接收多個(gè)實(shí)參,你可能會(huì)想先指定部分實(shí)參,余下的稍后再指定。

來看這個(gè)函數(shù):

function ajax(url,data,callback) {
    // ..
}

想象一個(gè)場景,你要發(fā)起多個(gè)已知 URL 的 API 請求,但這些請求的數(shù)據(jù)和處理響應(yīng)信息的回調(diào)函數(shù)要稍后才能知道。

當(dāng)然,你可以等到這些東西都確定后再發(fā)起 ajax(..) 請求,并且到那時(shí)再引用全局 URL 常量。但我們還有另一種選擇,就是創(chuàng)建一個(gè)已經(jīng)預(yù)設(shè) url 實(shí)參的函數(shù)引用。

我們將創(chuàng)建一個(gè)新函數(shù),其內(nèi)部仍然發(fā)起 ajax(..) 請求,此外在等待接收另外兩個(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ì)變得冗長乏味,特別是不同的預(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ú)有。但是,對函數(shù)式編程而言,這個(gè)行為的重要性是毋庸置疑的。

為了構(gòu)思這個(gè)用于實(shí)參預(yù)設(shè)的實(shí)用函數(shù),我們不僅要著眼于之前提到的手動(dòng)實(shí)現(xiàn)方式,還要在概念上審視一下到底發(fā)生了什么。

用一句話來說明發(fā)生的事情:getOrder(data,cb)ajax(url,data,cb) 函數(shù)的偏函數(shù)(partially-applied functions)。該術(shù)語代表的概念是:在函數(shù)調(diào)用現(xiàn)場(function call-site),將實(shí)參應(yīng)用(apply) 于形參。如你所見,我們一開始僅應(yīng)用了部分實(shí)參 —— 具體是將實(shí)參應(yīng)用到 url 形參 —— 剩下的實(shí)參稍后再應(yīng)用。

關(guān)于該模式更正式的說法是:偏函數(shù)嚴(yán)格來講是一個(gè)減少函數(shù)參數(shù)個(gè)數(shù)(arity)的過程;這里的參數(shù)個(gè)數(shù)指的是希望傳入的形參的數(shù)量。我們通過 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 );
    };
}

建議: 只是走馬觀花是不行的。請花些時(shí)間研究一下該實(shí)用函數(shù)中發(fā)生的事情。請確保你真的理解了。由于在接下來的文章里,我們將會(huì)一次又一次地提到該模式,所以你最好現(xiàn)在就適應(yīng)它。

partial(..) 函數(shù)接收 fn 參數(shù),來表示被我們偏應(yīng)用實(shí)參(partially apply)的函數(shù)。接著,fn 形參之后,presetArgs 數(shù)組收集了后面?zhèn)魅氲膶?shí)參,保存起來稍后使用。

我們創(chuàng)建并 return 了一個(gè)新的內(nèi)部函數(shù)(為了清晰明了,我們把它命名為partiallyApplied(..)),該函數(shù)中,laterArgs 數(shù)組收集了全部實(shí)參。

你注意到在內(nèi)部函數(shù)中的 fnpresetArgs 引用了嗎?他們是怎么如何工作的?在函數(shù) partial(..) 結(jié)束運(yùn)行后,內(nèi)部函數(shù)為何還能訪問 fnpresetArgs 引用?你答對了,就是因?yàn)?strong>閉包!內(nèi)部函數(shù) partiallyApplied(..) 封閉(closes over)了 fnpresetArgs 變量,所以無論該函數(shù)在哪里運(yùn)行,在 partial(..) 函數(shù)運(yùn)行后我們?nèi)匀豢梢栽L問這些變量。所以理解閉包是多么的重要!

當(dāng) partiallyApplied(..) 函數(shù)稍后在某處執(zhí)行時(shí),該函數(shù)使用被閉包作用(closed over)的 fn 引用來執(zhí)行原函數(shù),首先傳入(被閉包作用的)presetArgs 數(shù)組中所有的偏應(yīng)用(partial application)實(shí)參,然后再進(jìn)一步傳入 laterArgs 數(shù)組中的實(shí)參。

如果你對以上感到任何疑惑,請停下來再看一遍。相信我,隨著我們進(jìn)一步深入本文,你會(huì)欣然接受這個(gè)建議。

提一句,對于這類代碼,函數(shù)式編程者往往喜歡使用更簡短的 => 箭頭函數(shù)語法(請看第 2 章的 “語法” 小節(jié)),像這樣:

var partial =
    (fn, ...presetArgs) =>
        (...laterArgs) =>
            fn( ...presetArgs, ...laterArgs );

毫無疑問這更加簡潔,甚至代碼稀少。但我個(gè)人覺得,無論我們從數(shù)學(xué)符號(hào)的對稱性上獲得什么好處,都會(huì)因函數(shù)變成了匿名函數(shù)而在整體的可讀性上失去更多益處。此外,由于作用域邊界變得模糊,我們會(huì)更加難以辯認(rèn)閉包。

不管你喜歡哪種語法實(shí)現(xiàn)方式,現(xiàn)在我們用 partial(..) 實(shí)用函數(shù)來制造這些之前提及的偏函數(shù):

var getPerson = partial( ajax, "http://some.api/person" );

var getOrder = partial( ajax, "http://some.api/order" );

請暫停并思考一下 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)直接通過指定 urldata 兩個(gè)實(shí)參來定義 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)于下面的代碼,我們僅用這些代碼來確認(rèn)一下對兩個(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 );
}

再強(qiáng)調(diào)一下,為了確保你理解這些代碼段發(fā)生了什么,請暫停并重新閱讀一下它們。

注意: 第二個(gè)版本的函數(shù)包含了一個(gè)額外的函數(shù)包裝層。這看起來有些奇怪而且多余,但對于你真正要適應(yīng)的函數(shù)式編程來說,這僅僅是它的冰山一角。隨著本文的繼續(xù)深入,我們將會(huì)把許多函數(shù)互相包裝起來。記住,這就是函數(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ù)組對象內(nèi)置的 map(..) 實(shí)用函數(shù)。

[1,2,3,4,5].map( function adder(val){
    return add( 3, val );
} );
// [4,5,6,7,8]

注意: 如果你沒見過 map(..) ,別擔(dān)心,我們會(huì)在本書后面的部分詳細(xì)介紹它。目前你只需要知道它用來循環(huán)遍歷(loop over)一個(gè)數(shù)組,在遍歷過程中調(diào)用函數(shù)產(chǎn)出新值并存到新的數(shù)組中。

因?yàn)?add(..) 函數(shù)簽名不是 map(..) 函數(shù)所預(yù)期的,所以我們不直接把它傳入 map(..) 函數(shù)里。這樣一來,偏應(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è)功能。

對于下面的方案,你通常要傳 null 給用來綁定 this 的實(shí)參(第一個(gè)實(shí)參),而它是一個(gè)可以忽略的占位符。因此,這個(gè)方案非常糟糕。

請看:

var getPerson = ajax.bind( null, "http://some.api/person" );

那個(gè) null 只會(huì)給我?guī)頍o盡的煩惱。

將實(shí)參順序顛倒

回想我們之前調(diào)用 Ajax 函數(shù)的方式:ajax( url, data, cb )。如果要偏應(yīng)用 cb 而稍后再指定 dataurl 參數(shù),我們應(yīng)該怎么做呢?我們可以創(chuàng)建一個(gè)可以顛倒實(shí)參順序的實(shí)用函數(shù),用來包裝原函數(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í)參的順序了,接下來,我們不再從左邊開始,而是從右側(cè)開始偏應(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 } );

好,我們來定義一個(gè)從右邊開始偏應(yīng)用實(shí)參(譯者注:以下簡稱右偏應(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è)

我們來看一個(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)用分別拆解開來,這也許有助于我們理解整個(gè)過程:

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è)用來接收第二個(gè)參數(shù)的函數(shù)。而這個(gè)被返回的函數(shù)又只接收第二個(gè)參數(shù),并且返回一個(gè)接收第三個(gè)參數(shù)的函數(shù)。依此類推。

由此而知,柯里化將一個(gè)多參數(shù)(higher-arity)函數(shù)拆解為一系列的單元鏈?zhǔn)胶瘮?shù)。

如何定義一個(gè)用來柯里化的實(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ù))用來接收下一個(gè) nextArg 實(shí)參,與此同時(shí)將 args 實(shí)參集合作為唯一的 prevArgs 參數(shù)傳入 nextCurried(..) 函數(shù)。一旦我們收集了足夠長度的 args 數(shù)組,就用這些實(shí)參觸發(fā)原函數(shù) fn(..)。

默認(rèn)地,我們的實(shí)現(xiàn)方案基于下面的條件:在拿到原函數(shù)期望的全部實(shí)參之前,我們能夠通過檢查將要被柯里化的函數(shù)的 length 屬性來得知柯里化需要迭代多少次。

假如你將該版本的 curry(..) 函數(shù)用在一個(gè) length 屬性不明確的函數(shù)上 —— 函數(shù)的形參聲明包含默認(rèn)形參值、形參解構(gòu),或者它是可變參數(shù)函數(shù),用 ...args 當(dāng)形參;參考第 2 章 —— 你將要傳入 arity 參數(shù)(作為 curry(..) 的第二個(gè)形參)來確保 curry(..) 函數(shù)的正常運(yùn)行。

我們用 curry(..) 函數(shù)來實(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)用相似的,所以我們可以用幾乎相同的方式以柯里化來完成那個(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]

讓我們來看看另一個(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 用來指定需要鏈?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)用來每次指定一個(gè)形參,你得在每個(gè)函數(shù)中逐次調(diào)用 partialApply(..) 函數(shù)。而被柯里化的函數(shù)可以自動(dòng)完成這個(gè)工作,這讓一次多帶帶傳遞一個(gè)參數(shù)變得更加符合人機(jī)工程學(xué)。

在 JavaScript 中,柯里化和偏應(yīng)用都使用閉包來保存實(shí)參,直到收齊所有實(shí)參后我們再執(zhí)行原函數(shù)。

柯里化和偏應(yīng)用有什么用?

無論是柯里化風(fēng)格(sum(1)(2)(3))還是偏應(yīng)用風(fēng)格(partial(sum,1,2)(3)),它們的簽名比普通函數(shù)簽名奇怪得多。那么,在適應(yīng)函數(shù)式編程的時(shí)候,我們?yōu)槭裁匆@么做呢?答案有幾個(gè)方面。

首先是顯而易見的理由,使用柯里化和偏應(yīng)用可以將指定分離實(shí)參的時(shí)機(jī)和地方獨(dú)立開來(遍及代碼的每一處),而傳統(tǒng)函數(shù)調(diào)用則需要預(yù)先確定所有實(shí)參。如果你在代碼某一處只獲取了部分實(shí)參,然后在另一處確定另一部分實(shí)參,這個(gè)時(shí)候柯里化和偏應(yī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ì)讓我們處理起來更簡單。我們將在后面繼續(xù)探討這個(gè)話題。

如何柯里化多個(gè)實(shí)參?

到目前為止,我相信我給出的是我們能在 JavaScript 中能得到的,最精髓的柯里化定義和實(shí)現(xiàn)方式。

具體來說,如果簡單看下柯里化在 Haskell 語言中的應(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ù)的值都分別傳入了多帶帶的柯里化調(diào)用。在 JS 中看起來則會(huì)是這樣:foo(1)(2)(3)。這和我此前講過的 curry(..) 風(fēng)格如出一轍。

注意: 在 Haskell 中,foo (1,2,3) 不是把三個(gè)值當(dāng)作多帶帶的實(shí)參一次性傳入函數(shù),而是把它們包含在一個(gè)元組(類似 JS 數(shù)組)中作為多帶帶實(shí)參傳入函數(shù)。為了正常運(yùn)行,我們需要改變 foo 函數(shù)來處理作為實(shí)參的元組。據(jù)我所知,在 Haskell 中我們沒有辦法在一次函數(shù)調(diào)用中將全部三個(gè)實(shí)參獨(dú)立地傳入,而需要柯里化調(diào)用每個(gè)函數(shù)。誠然,多次調(diào)用對于 Haskell 開發(fā)者來說是透明的,但對 JS 開發(fā)者來說,這在語法上更加一目了然。

基于以上原因,我認(rèn)為此前展示的 curry(..) 函數(shù)是一個(gè)對 Haskell 柯里化的可靠改編,我把它叫做 “嚴(yán)格柯里化”。

然而,我們需要注意,大多數(shù)流行的 JavaScript 函數(shù)式編程庫都使用了一種并不嚴(yán)格的柯里化(loose currying)定義。

具體來說,往往 JS 柯里化實(shí)用函數(shù)會(huì)允許你在每次柯里化調(diào)用中指定多個(gè)實(shí)參。回顧一下之前提到的 sum(..) 示例,松散柯里化應(yīng)用會(huì)是下面這樣:

var curriedSum = looseCurry( sum, 5 );

curriedSum( 1 )( 2, 3 )( 4, 5 );            // 15

可以看到,語法上我們節(jié)省了()的使用,并且把五次函數(shù)調(diào)用減少成三次,間接提高了性能。除此之外,使用 looseCurry(..) 函數(shù)的結(jié)果也和之前更加狹義的 curry(..) 函數(shù)一樣。我猜便利性和性能因素是眾框架允許多實(shí)參柯里化的原因。這看起來更像是品味問題。

注意: 松散柯里化允許你傳入超過形參數(shù)量(arity,原函數(shù)確認(rèn)或指定的形參數(shù)量)的實(shí)參。如果你將函數(shù)的參數(shù)設(shè)計(jì)成可配的或變化的,那么松散柯里化將會(huì)有利于你。例如,如果你將要柯里化的函數(shù)接收 5 個(gè)實(shí)參,松散柯里化依然允許傳入超過 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)這種常見的更松散的定義:

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ù)的來完成。

反柯里化

你也會(huì)遇到這種情況:拿到一個(gè)柯里化后的函數(shù),卻想要它柯里化之前的版本 —— 這本質(zhì)上就是想將類似 f(1)(2)(3) 的函數(shù)變回類似 g(1,2,3) 的函數(shù)。

不出意料的話,處理這個(gè)需求的標(biāo)準(zhǔn)實(shí)用函數(shù)通常被叫作 uncurry(..)。下面是簡陋的實(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;
        };

警告: 請不要以為 uncurry(curry(f))f 函數(shù)的行為完全一樣。雖然在某些庫中,反柯里化使函數(shù)變成和原函數(shù)(譯者注:這里的原函數(shù)指柯里化之前的函數(shù))類似的函數(shù),但是凡事皆有例外,我們這里就有一個(gè)例外。如果你傳入原函數(shù)期望數(shù)量的實(shí)參,那么在反柯里化后,函數(shù)的行為(大多數(shù)情況下)和原函數(shù)相同。然而,如果你少傳了實(shí)參,就會(huì)得到一個(gè)仍然在等待傳入更多實(shí)參的部分柯里化函數(shù)。我們在下面的代碼中說明這個(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ù)最為常見的作用對象很可能并不是人為生成的柯里化函數(shù)(例如上文所示),而是某些操作所產(chǎn)生的已經(jīng)被柯里化了的結(jié)果函數(shù)。我們將在本章后面關(guān)于 “無形參風(fēng)格” 的討論中闡述這種應(yīng)用場景。

只要一個(gè)實(shí)參

設(shè)想你向一個(gè)實(shí)用函數(shù)傳入一個(gè)函數(shù),而這個(gè)實(shí)用函數(shù)會(huì)把多個(gè)實(shí)參傳入函數(shù),但可能你只希望你的函數(shù)接收單一實(shí)參。如果你有個(gè)類似我們前面提到被松散柯里化的函數(shù),它能接收多個(gè)實(shí)參,但你卻想讓它接收單一實(shí)參。那么這就是我想說的情況。

我們可以設(shè)計(jì)一個(gè)簡單的實(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ù)打過照面了。它調(diào)用傳入其中的 mapping 函數(shù)時(shí)會(huì)傳入三個(gè)實(shí)參:value、indexlist。如果你希望你傳入 map(..) 的 mapping 函數(shù)只接收一個(gè)參數(shù),比如 value,你可以使用 unary(..) 函數(shù)來操作:

function unary(fn) {
    return function onlyOneArg(arg){
        return fn( arg );
    };
}

var adder = looseCurry( sum, 2 );

// 出問題了:
[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]

對于 parseInt(str,radix) 這個(gè)函數(shù)調(diào)用,如果 map(..) 函數(shù)調(diào)用它時(shí)在它的第二個(gè)實(shí)參位置傳入 index,那么毫無疑問 parseInt(..) 會(huì)將 index 理解為 radix 參數(shù),這是我們不希望發(fā)生的。而 unary(..) 函數(shù)創(chuàng)建了一個(gè)只接收第一個(gè)傳入實(shí)參,忽略其他實(shí)參的新函數(shù),這就意味著傳入 index 不再會(huì)被誤解為 radix 參數(shù)。

傳一個(gè)返回一個(gè)

說到只傳一個(gè)實(shí)參的函數(shù),在函數(shù)式編程工具庫中有另一種通用的基礎(chǔ)函數(shù):該函數(shù)接收一個(gè)實(shí)參,然后什么都不做,原封不動(dòng)地返回實(shí)參值。

function identity(v) {
    return v;
}

// ES6 箭頭函數(shù)形式
var identity =
    v =>
        v;

看起來這個(gè)實(shí)用函數(shù)簡單到了無處可用的地步。但即使是簡單的函數(shù)在函數(shù)式編程的世界里也能發(fā)揮作用。就像演藝圈有句諺語:沒有小角色,只有小演員。

舉個(gè)例子,想象一下你要用正則表達(dá)式拆分(split up)一個(gè)字符串,但輸出的數(shù)組中可能包含一些空值。我們可以使用 filter(..) 數(shù)組方法(下文會(huì)詳細(xì)說到這個(gè)方法)來篩除空值,而我們將 identity(..) 函數(shù)作為 filter(..) 的斷言:

var words = "   Now is the time for all...  ".split( /s|/ );
words;
// ["","Now","is","the","time","for","all","...",""]

words.filter( identity );
// ["Now","is","the","time","for","all","..."]

既然 identity(..) 會(huì)簡單地返回傳入的值,而 JS 會(huì)將每個(gè)值強(qiáng)制轉(zhuǎn)換為 truefalse,這樣我們就能在最終的數(shù)組里對每個(gè)值進(jìn)行保存或排除。

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

另一個(gè)使用 identity(..) 的示例就是將其作為替代一個(gè)轉(zhuǎn)換函數(shù)(譯者注:transformation,這里指的是對傳入值進(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è)問題的 “解決方案”。但我這有一個(gè)函數(shù)式編程實(shí)用函數(shù)可以完美勝任該任務(wù):

function constant(v) {
    return function value(){
        return v;
    };
}

// or the ES6 => form
var constant =
    v =>
        () =>
            v;

這個(gè)微小而簡潔的實(shí)用函數(shù)可以解決我們關(guān)于 then(..) 的煩惱:

p1.then( foo ).then( () => p2 ).then( bar );

// 對比:

p1.then( foo ).then( constant( p2 ) ).then( bar );

警告: 盡管使用 () => p2 箭頭函數(shù)的版本比使用 constant(p2) 的版本更簡短,但我建議你忍住別用前者。該箭頭函數(shù)返回了一個(gè)來自外作用域的值,這和 函數(shù)式編程的理念有些矛盾。我們將會(huì)在后面第 5 章的 “減少副作用” 小節(jié)中提到這種行為帶來的陷阱。

擴(kuò)展在參數(shù)中的妙用

在第 2 章中,我們簡要地講到了形參數(shù)組解構(gòu)?;仡櫼幌略撌纠?/p>

function foo( [x,y,...args] ) {
    // ..
}

foo( [1,2,3] );

foo(..) 函數(shù)的形參列表中,我們期望接收單一數(shù)組實(shí)參,我們要把這個(gè)數(shù)組拆解 —— 或者更貼切地說,擴(kuò)展(spread out)—— 成獨(dú)立的實(shí)參 xy。除了頭兩個(gè)位置以外的參數(shù)值我們都會(huì)通過 ... 操作將它們收集在 args 數(shù)組中。

當(dāng)函數(shù)必須接收一個(gè)數(shù)組,而你卻想把數(shù)組內(nèi)容當(dāng)成多帶帶形參來處理的時(shí)候,這個(gè)技巧十分有用。

然而,有的時(shí)候,你無法改變原函數(shù)的定義,但想使用形參數(shù)組解構(gòu)。舉個(gè)例子,請思考下面的函數(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(..) 期望接收多帶帶的 xy 形參。如果我們可以把 foo(..) 的函數(shù)聲明改變成 function foo([x,y]) { .. 那就好辦了?;蛘?,我們可以改變 bar(..) 函數(shù)的行為,把調(diào)用改成 fn(...[3,9]),這樣就能將 39 分別傳入 foo(..) 函數(shù)了。

假設(shè)有兩個(gè)在此方法上互不兼容的函數(shù),而且由于各種原因你無法改變它們的聲明和定義。那么你該如何一并使用它們呢?

為了調(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(..),但一些庫,比如 Ramda,經(jīng)常把它叫做 apply(..)。

現(xiàn)在我們可以使用 spreadArgs(..) 來調(diào)整 foo(..) 函數(shù),使其作為一個(gè)合適的輸入?yún)?shù)并正常地工作:

bar( spreadArgs( foo ) );            // 12

相信我,雖然我不能講清楚這些問題出現(xiàn)的原因,但它們一定會(huì)出現(xiàn)的。本質(zhì)上,spreadArgs(..) 函數(shù)使我們能夠定義一個(gè)借助數(shù)組 return 多個(gè)值的函數(shù),不過,它讓這些值仍然能分別作為其他函數(shù)的輸入?yún)?shù)來處理。

一個(gè)函數(shù)的輸出作為另外一個(gè)函數(shù)的輸入被稱作組合(composition),我們將在第四章詳細(xì)討論這個(gè)話題。

盡管我們在談?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ù)語 “擴(kuò)展(spread)” 和 “聚集(gather)” 可以把這兩個(gè)函數(shù)發(fā)生的事情解釋得更好一些。

因?yàn)橛袝r(shí)我們可能要調(diào)整一個(gè)函數(shù),解構(gòu)其數(shù)組形參,使其成為另一個(gè)分別接收多帶帶實(shí)參的函數(shù),所以我們可以通過使用 gatherArgs(..) 實(shí)用函數(shù)來將多帶帶的實(shí)參聚集到一個(gè)數(shù)組中。我們將在第 8 章中細(xì)說 reduce(..) 函數(shù),這里我們簡要說一下:它重復(fù)調(diào)用傳入的 reducer 函數(shù),其中 reducer 函數(shù)有兩個(gè)形參,現(xiàn)在我們可以將這兩個(gè)形參聚集起來:

function combineFirstTwo([ v1, v2 ]) {
    return v1 + v2;
}

[1,2,3,4,5].reduce( gatherArgs( combineFirstTwo ) );
// 15
參數(shù)順序的那些事兒

對于多形參函數(shù)的柯里化和偏應(yīng)用,我們不得不通過許多令人懊惱的技巧來修正這些形參的順序。有時(shí)我們把一個(gè)函數(shù)的形參順序定義成柯里化需求的形參順序,但這種順序沒有兼容性,我們不得不絞盡腦汁來重新調(diào)整它。

讓人沮喪的可不僅是我們需要使用實(shí)用函數(shù)來委曲求全,在此之外,這種做法還會(huì)導(dǎo)致我們的代碼被無關(guān)代碼混淆。這種東西就像碎紙片,這一片那一片的,而不是一整個(gè)突出問題,但這些問題的細(xì)碎絲毫不會(huì)減少它們帶來的苦惱。

難道就沒有能讓我們從修正參數(shù)順序這件事里解脫出來的方法嗎?。?/p>

在第 2 章里,我們講到了命名實(shí)參(named-argument)解構(gòu)模式。回顧一下:

function foo( {x,y} = {} ) {
    console.log( x, y );
}

foo( {
    y: 3
} );                    // undefined 3

我們將 foo(..) 函數(shù)的第一個(gè)形參 —— 它被期望是一個(gè)對象 —— 解構(gòu)成多帶帶的形參 xy。接著在調(diào)用時(shí)傳入一個(gè)對象實(shí)參,并且提供函數(shù)期望的屬性,這樣就可以把 “命名實(shí)參” 映射到相應(yīng)形參上。

命名實(shí)參主要的好處就是不用再糾結(jié)實(shí)參傳入的順序,因此提高了可讀性。我們可以發(fā)掘一下看看是否能設(shè)計(jì)一個(gè)等效的實(shí)用函數(shù)來處理對象屬性,以此提高柯里化和偏應(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ā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ú)立的(沒有經(jīng)過形參解構(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ù),它接收對象實(shí)參的 key: value 鍵值對,并將其 “擴(kuò)展” 成獨(dú)立實(shí)參。

不過,我們需要注意某些異常的地方。我們使用 spreadArgs(..) 函數(shù)處理數(shù)組實(shí)參時(shí),參數(shù)的順序是明確的。然而,對象屬性的順序是不太明確且不可靠的。取決于不同對象的創(chuàng)建方式和屬性設(shè)置方式,我們無法完全確認(rèn)對象會(huì)產(chǎn)生什么順序的屬性枚舉。

針對這個(gè)問題,我們定義的實(shí)用函數(shù)需要讓你能夠指定函數(shù)期望的實(shí)參順序(比如屬性枚舉的順序)。我們可以傳入一個(gè)類似 ["x","y","z"] 的數(shù)組,通知實(shí)用函數(shù)基于該數(shù)組的順序來獲取對象實(shí)參的屬性值。

這著實(shí)不錯(cuò),但還是有點(diǎn)瑕疵,就算是最簡單的函數(shù),我們也免不了為其增添一個(gè)由屬性名構(gòu)成的數(shù)組。難道我們就沒有一種可以探知函數(shù)形參順序的技巧嗎?哪怕給一個(gè)普通而簡單的例子?還真有!

JavaScript 的函數(shù)對象上有一個(gè) .toString() 方法,它返回函數(shù)代碼的字符串形式,其中包括函數(shù)聲明的簽名。先忽略其正則表達(dá)式分析技巧,我們可以通過解析函數(shù)字符串來獲取每個(gè)多帶帶的命名形參。雖然這段代碼看起來有些粗暴,但它足以滿足我們的需求:

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ù)解析邏輯并非無懈可擊,使用正則來解析代碼這個(gè)前提就已經(jīng)很不靠譜了!但處理一般情況是我們的唯一目標(biāo),從這點(diǎn)來看這個(gè)實(shí)用函數(shù)還是恰到好處的。我們需要的只是對簡單形參(包括帶默認(rèn)值的形參)函數(shù)的形參順序做一個(gè)恰當(dāng)?shù)哪J(rèn)檢測。例如,我們的實(shí)用函數(shù)不需要把復(fù)雜的解構(gòu)形參給解析出來,因?yàn)闊o論如何我們不太可能對擁有這種復(fù)雜形參的函數(shù)使用 spreadArgProps() 函數(shù)。因此該邏輯能搞定 80% 的需求,它允許我們在其它不能正確解析復(fù)雜函數(shù)簽名的情況下覆蓋 propOrder 數(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)的對象形參(object parameters)和命名實(shí)參(named arguments)模式,通過減少由調(diào)整實(shí)參順序帶來的干擾,明顯地提高了代碼的可讀性,不過據(jù)我所知,沒有哪個(gè)主流的函數(shù)式編程庫使用該方案。所以你會(huì)看到該做法與大多數(shù) JavaScript 函數(shù)式編程很不一樣.

此外,使用在這種風(fēng)格下定義的函數(shù)要求你知道每個(gè)實(shí)參的名字。你必須記?。骸斑@個(gè)函數(shù)形參叫作 ‘fn’ ”,而不是只記得:“噢,把這個(gè)函數(shù)作為第一個(gè)實(shí)參傳進(jìn)去”。

請小心地權(quán)衡它們。

無形參風(fēng)格

在函數(shù)式編程的世界中,有一種流行的代碼風(fēng)格,其目的是通過移除不必要的形參-實(shí)參映射來減少視覺上的干擾。這種風(fēng)格的正式名稱為 “隱性編程(tacit programming)”,一般則稱作 “無形參(point-free)” 風(fēng)格。術(shù)語 “point” 在這里指的是函數(shù)形參。

警告: 且慢,先說明我們這次的討論是一個(gè)有邊界的提議,我不建議你在函數(shù)式編程的代碼里不惜代價(jià)地濫用無形參風(fēng)格。該技術(shù)是用于在適當(dāng)情況下提升可讀性。但你完全可能像濫用軟件開發(fā)里大多數(shù)東西一樣濫用它。如果你由于必須遷移到無參數(shù)風(fēng)格而讓代碼難以理解,請打住。你不會(huì)因此獲得小紅花,因?yàn)槟阌每此坡斆鞯逎y懂的方式抹除形參這個(gè)點(diǎn)的同時(shí),還抹除了代碼的重點(diǎn)。

我們從一個(gè)簡單的例子開始:

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ù)包裝層是非必需的。我們可以將其簡化為無形參風(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 來進(jìn)行整數(shù)解析。該例子中我們可以借助 unary(..) 函數(shù):

["1","2","3"].map( unary( parseInt ) );
// [1,2,3]

使用無形參風(fēng)格的關(guān)鍵,是找到你代碼中,有哪些地方的函數(shù)直接將其形參作為內(nèi)部函數(shù)調(diào)用的實(shí)參。以上提到的兩個(gè)例子中,mapper(..) 函數(shù)拿到形參 v 多帶帶傳入了另一個(gè)函數(shù)調(diào)用。我們可以借助 unary(..) 函數(shù)將提取形參的邏輯層替換成無參數(shù)形式表達(dá)式。

警告: 你可能跟我一樣,已經(jīng)嘗試著使用 map(partialRight(parseInt,10)) 來將 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ì)對頭兩個(gè)實(shí)參作出反應(yīng)。

來看另一個(gè)例子:

// 將 `console.log` 當(dāng)成一個(gè)函數(shù)使用
// 便于避免潛在的綁定問題

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)信息足夠長時(shí),將它打印出來,換而言之,我們需要一個(gè) !isShortEnough(..) 斷言。你可能會(huì)首先想到:

function isLongEnough(str) {
    return !isShortEnough( str );
}

printIf( isLongEnough, msg1 );
printIf( isLongEnough, msg2 );            // Hello World

這太簡單了...但現(xiàn)在我們的重點(diǎn)來了!你看到了 str 形參是如何傳遞的嗎?我們能否不通過重新實(shí)現(xiàn) str.length 的檢查邏輯,而重構(gòu)代碼并使其變成無形參風(fēng)格呢?

我們定義一個(gè) not(..) 取反輔助函數(shù)(在函數(shù)式編程庫中又被稱作 complement(..)):

function not(predicate) {
    return function negated(...args){
        return !predicate( ...args );
    };
}

// ES6 箭頭函數(shù)形式
var not =
    predicate =>
        (...args) =>
            !predicate( ...args );

接著,我們使用 not(..) 函數(shù)來定義無形參的 isLongEnough(..) 函數(shù):

var isLongEnough = not( isShortEnough );

printIf( isLongEnough, msg2 );            // Hello World

目前為止已經(jīng)不錯(cuò)了,但還能更進(jìn)一步。我們實(shí)際上可以將 printIf(..) 函數(shù)本身重構(gòu)成無形參風(fēng)格。

我們可以用 when(..) 實(shí)用函數(shù)來表示 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é)合起來搞定無形參風(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ù),看起來就是這樣:fn(predicate)(str)。

多個(gè)(兩個(gè))鏈?zhǔn)胶瘮?shù)的調(diào)用看起來很挫,就像被柯里化的函數(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

但愿無形參風(fēng)格編程的函數(shù)式編程實(shí)踐逐漸變得更有意義。你仍然可以通過大量實(shí)踐來訓(xùn)練自己,讓自己接受這種風(fēng)格。再次提醒,請三思而后行,掂量一下是否值得使用無形參風(fēng)格編程,以及使用到什么程度會(huì)益于提高代碼的可讀性。

有形參還是無形參,你怎么選?

注意: 還有什么無形參風(fēng)格編程的實(shí)踐呢?我們將在第 4 章的 “回顧形參” 小節(jié)里,站在新學(xué)習(xí)的組合函數(shù)知識(shí)之上來回顧這個(gè)技術(shù)。

總結(jié)

偏應(yīng)用是用來減少函數(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ǔ)工具庫的一部分。

無形參是一種書寫代碼的風(fēng)格,這種風(fēng)格移除了非必需的形參映射實(shí)參邏輯,其目的在于提高代碼的可讀性和可理解性。

【上一章】翻譯連載 |《JavaScript 輕量級(jí)函數(shù)式編程》- 第 2 章:函數(shù)基礎(chǔ)
【下一章】翻譯連載 |《你不知道的JS》姊妹篇 |《JavaScript 輕量級(jí)函數(shù)式編程》- 第4章:組合函數(shù)

iKcamp原創(chuàng)新書《移動(dòng)Web前端高效開發(fā)實(shí)戰(zhàn)》已在亞馬遜、京東、當(dāng)當(dāng)開售。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/85184.html

相關(guān)文章

  • 翻譯連載 |《你不知道JS妹篇 |《JavaScript 量級(jí)函數(shù)編程》- 引言&前言

    摘要:我稱之為輕量級(jí)函數(shù)式編程。序眾所周知,我是一個(gè)函數(shù)式編程迷。函數(shù)式編程有很多種定義。本書是你開啟函數(shù)式編程旅途的絕佳起點(diǎn)。事實(shí)上,已經(jīng)有很多從頭到尾正確的方式介紹函數(shù)式編程的書了。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson?。 禮ou-Dont-Know-JS》作者 譯者團(tuán)隊(duì)(排名不分先后):阿希、blueken、brucecham、...

    2bdenny 評論0 收藏0
  • 翻譯連載 | 9 :遞歸(上)-《JavaScript量級(jí)函數(shù)編程》 |《你不知道JS

    摘要:一旦我們滿足了基本條件值為,我們將不再調(diào)用遞歸函數(shù),只是有效地執(zhí)行了。遞歸深諳函數(shù)式編程之精髓,最被廣泛引證的原因是,在調(diào)用棧中,遞歸把大部分顯式狀態(tài)跟蹤換為了隱式狀態(tài)。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅(jiān)實(shí)的梁柱;...

    MasonEast 評論0 收藏0
  • 翻譯連載 |《你不知道JS妹篇 |《JavaScript 量級(jí)函數(shù)編程》- 1

    摘要:所以我覺得函數(shù)式編程領(lǐng)域更像學(xué)者的領(lǐng)域。函數(shù)式編程的原則是完善的,經(jīng)過了深入的研究和審查,并且可以被驗(yàn)證。函數(shù)式編程是編寫可讀代碼的最有效工具之一可能還有其他。我知道很多函數(shù)式編程編程者會(huì)認(rèn)為形式主義本身有助于學(xué)習(xí)。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson?。 禮ou-Dont-Know-JS》作者 關(guān)于譯者:這是一個(gè)流淌著滬江血液...

    omgdog 評論0 收藏0
  • 翻譯連載 | 10 :異步函數(shù)(上)-《JavaScript量級(jí)函數(shù)編程》 |《你不

    摘要:這就是積極的函數(shù)式編程。上一章翻譯連載第章遞歸下輕量級(jí)函數(shù)式編程你不知道的姊妹篇原創(chuàng)新書移動(dòng)前端高效開發(fā)實(shí)戰(zhàn)已在亞馬遜京東當(dāng)當(dāng)開售。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅(jiān)實(shí)的梁柱;分享,是 CSS 里最閃耀的一瞥;總...

    Lucky_Boy 評論0 收藏0
  • 翻譯連載 |《你不知道JS妹篇 |《JavaScript 量級(jí)函數(shù)編程》- 2 :函

    摘要:從某些方面來講,這章回顧的函數(shù)知識(shí)并不是針對函數(shù)式編程者,非函數(shù)式編程者同樣需要了解。什么是函數(shù)針對函數(shù)式編程,很自然而然的我會(huì)想到從函數(shù)開始。如果你計(jì)劃使用函數(shù)式編程,你應(yīng)該盡可能多地使用函數(shù),而不是程序。指的是一個(gè)函數(shù)聲明的形參數(shù)量。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson?。 禮ou-Dont-Know-JS》作者 關(guān)于譯者:...

    Riddler 評論0 收藏0

發(fā)表評論

0條評論

閱讀需要支付1元查看
<