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

来源:http://www.cnblogs.com/ikcamp/archive/2017/09/19/7552941.html
-Advertisement-
Play Games

在第 2 章的 “函數輸入” 小節中,我們聊到了函數形參(parameters)和實參(arguments)的基本知識,實際上還瞭解到一些能簡化其使用方式的語法技巧,比如 `...` 操作符和解構(destructuring)。 在那個討論中,我建議儘可能設計單一形參的函數。但實際上你不能每次都做... ...


Hello跟我學

關於譯者:這是一個流淌著滬江血液的純粹工程:認真,是 HTML 最堅實的梁柱;分享,是 CSS 里最閃耀的一瞥;總結,是 JavaScript 中最嚴謹的邏輯。經過捶打磨練,成就了本書的中文版。本書包含了函數式編程之精髓,希望可以幫助大家在學習函數式編程的道路上走的更順暢。比心。

譯者團隊(排名不分先後):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry蘿蔔vavd317vivaxy萌萌zhouyao

第 3 章:管理函數的輸入(Inputs)

在第 2 章的 “函數輸入” 小節中,我們聊到了函數形參(parameters)和實參(arguments)的基本知識,實際上還瞭解到一些能簡化其使用方式的語法技巧,比如 ... 操作符和解構(destructuring)。

在那個討論中,我建議儘可能設計單一形參的函數。但實際上你不能每次都做到,而且也不能每次都掌控你的函數簽名(譯者註:JS 中,函數簽名一般包含函數名和形參等函數關鍵信息,例如 foo(a, b = 1, c))。

現在,我們把註意力放在更複雜、強大的模式上,以便討論處在這些場景下的函數輸入。

立即傳參和稍後傳參

如果一個函數接收多個實參,你可能會想先指定部分實參,餘下的稍後再指定。

來看這個函數:

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

想象一個場景,你要發起多個已知 URL 的 API 請求,但這些請求的數據和處理響應信息的回調函數要稍後才能知道。

當然,你可以等到這些東西都確定後再發起 ajax(..) 請求,並且到那時再引用全局 URL 常量。但我們還有另一種選擇,就是創建一個已經預設 url 實參的函數引用。

我們將創建一個新函數,其內部仍然發起 ajax(..) 請求,此外在等待接收另外兩個實參的同時,我們手動將 ajax(..) 第一個實參設置成你關心的 API 地址。

function getPerson(data,cb) {
    ajax( "http://some.api/person", data, cb );
}

function getOrder(data,cb) {
    ajax( "http://some.api/order", data, cb );
}

手動指定這些外層函數當然是完全有可能的,但這可能會變得冗長乏味,特別是不同的預設實參還會變化的時候,譬如:

function getCurrentUser(cb) {
    getPerson( { user: CURRENT_USER_ID }, cb );
}

函數式編程者習慣於在重覆做同一種事情的地方找到模式,並試著將這些行為轉換為邏輯可重用的實用函數。實際上,該行為肯定已是大多數讀者的本能反應了,所以這並非函數式編程獨有。但是,對函數式編程而言,這個行為的重要性是毋庸置疑的。

為了構思這個用於實參預設的實用函數,我們不僅要著眼於之前提到的手動實現方式,還要在概念上審視一下到底發生了什麼。

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

關於該模式更正式的說法是:偏函數嚴格來講是一個減少函數參數個數(arity)的過程;這裡的參數個數指的是希望傳入的形參的數量。我們通過 getOrder(..) 把原函數 ajax(..) 的參數個數從 3 個減少到了 2 個。

讓我們定義一個 partial(..) 實用函數:

function partial(fn,...presetArgs) {
    return function partiallyApplied(...laterArgs){
        return fn( ...presetArgs, ...laterArgs );
    };
}

建議: 只是走馬觀花是不行的。請花些時間研究一下該實用函數中發生的事情。請確保你真的理解了。由於在接下來的文章里,我們將會一次又一次地提到該模式,所以你最好現在就適應它。

partial(..) 函數接收 fn 參數,來表示被我們偏應用實參(partially apply)的函數。接著,fn 形參之後,presetArgs 數組收集了後面傳入的實參,保存起來稍後使用。

我們創建並 return 了一個新的內部函數(為了清晰明瞭,我們把它命名為partiallyApplied(..)),該函數中,laterArgs 數組收集了全部實參。

你註意到在內部函數中的 fnpresetArgs 引用了嗎?他們是怎麼如何工作的?在函數 partial(..) 結束運行後,內部函數為何還能訪問 fnpresetArgs 引用?你答對了,就是因為閉包!內部函數 partiallyApplied(..) 封閉(closes over)了 fnpresetArgs 變數,所以無論該函數在哪裡運行,在 partial(..) 函數運行後我們仍然可以訪問這些變數。所以理解閉包是多麼的重要!

partiallyApplied(..) 函數稍後在某處執行時,該函數使用被閉包作用(closed over)的 fn 引用來執行原函數,首先傳入(被閉包作用的)presetArgs 數組中所有的偏應用(partial application)實參,然後再進一步傳入 laterArgs 數組中的實參。

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

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

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

毫無疑問這更加簡潔,甚至代碼稀少。但我個人覺得,無論我們從數學符號的對稱性上獲得什麼好處,都會因函數變成了匿名函數而在整體的可讀性上失去更多益處。此外,由於作用域邊界變得模糊,我們會更加難以辯認閉包。

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

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

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

請暫停並思考一下 getPerson(..) 函數的外形和內在。它相當於下麵這樣:

var getPerson = function partiallyApplied(...laterArgs) {
    return ajax( "http://some.api/person", ...laterArgs );
};

創建 getOrder(..) 函數可以依葫蘆畫瓢。但是 getCurrentUser(..) 函數又如何呢?

// 版本 1
var getCurrentUser = partial(
    ajax,
    "http://some.api/person",
    { user: CURRENT_USER_ID }
);

// 版本 2
var getCurrentUser = partial( getPerson, { user: CURRENT_USER_ID } );

我們可以(版本 1)直接通過指定 urldata 兩個實參來定義 getCurrentUser(..) 函數,也可以(版本 2)將 getCurrentUser(..) 函數定義成 getPerson(..) 的偏應用,該偏應用僅指定一個附加的 data 實參。

因為版本 2 重用了已經定義好的函數,所以它在表達上更清晰一些。因此我認為它更加貼合函數式編程精神。

版本 1 和 2 分別相當於下麵的代碼,我們僅用這些代碼來確認一下對兩個函數版本內部運行機制的理解。

// 版本 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 );
}

再強調一下,為了確保你理解這些代碼段發生了什麼,請暫停並重新閱讀一下它們。

註意: 第二個版本的函數包含了一個額外的函數包裝層。這看起來有些奇怪而且多餘,但對於你真正要適應的函數式編程來說,這僅僅是它的冰山一角。隨著本文的繼續深入,我們將會把許多函數互相包裝起來。記住,這就是函數式編程!

我們接著看另外一個偏應用的實用示例。設想一個 add(..) 函數,它接收兩個實參,並取二者之和:

function add(x,y) {
    return x + y;
}

現在,想象我們要拿到一個數字列表,並且給其中每個數字加一個確定的數值。我們將使用 JS 數組對象內置的 map(..) 實用函數。

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

註意: 如果你沒見過 map(..) ,別擔心,我們會在本書後面的部分詳細介紹它。目前你只需要知道它用來迴圈遍歷(loop over)一個數組,在遍歷過程中調用函數產出新值並存到新的數組中。

因為 add(..) 函數簽名不是 map(..) 函數所預期的,所以我們不直接把它傳入 map(..) 函數里。這樣一來,偏應用就有了用武之地:我們可以調整 add(..) 函數簽名,以符合 map(..) 函數的預期。

[1,2,3,4,5].map( partial( add, 3 ) );
// [4,5,6,7,8]

bind(..)

JavaScript 有一個內建的 bind(..) 實用函數,任何函數都可以使用它。該函數有兩個功能:預設 this 關鍵字的上下文,以及偏應用實參。

我認為將這兩個功能混合進一個實用函數是極其糟糕的決定。有時你不想關心 this 的綁定,而只是要偏應用實參。我本人基本上從不會同時需要這兩個功能。

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

請看:

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

那個 null 只會給我帶來無盡的煩惱。

將實參順序顛倒

回想我們之前調用 Ajax 函數的方式:ajax( url, data, cb )。如果要偏應用 cb 而稍後再指定 dataurl 參數,我們應該怎麼做呢?我們可以創建一個可以顛倒實參順序的實用函數,用來包裝原函數。

function reverseArgs(fn) {
    return function argsReversed(...args){
        return fn( ...args.reverse() );
    };
}

// ES6 箭頭函數形式
var reverseArgs =
    fn =>
        (...args) =>
            fn( ...args.reverse() );

現在可以顛倒 ajax(..) 實參的順序了,接下來,我們不再從左邊開始,而是從右側開始偏應用實參。為了恢復期望的實參順序,接著我們又將偏應用實參後的函數顛倒一下實參順序:

var cache = {};

var cacheResult = reverseArgs(
    partial( reverseArgs( ajax ), function onResult(obj){
        cache[obj.id] = obj;
    } )
);

// 處理後:
cacheResult( "http://some.api/person", { user: CURRENT_USER_ID } );

好,我們來定義一個從右邊開始偏應用實參(譯者註:以下簡稱右偏應用實參)的 partialRight(..) 實用函數。我們將運用和上面相同的技巧於該函數中:

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 } );

這個 partialRight(..) 函數的實現方案不能保證讓一個特定的形參接收特定的被偏應用的值;它只能確保將被這些值(一個或幾個)當作原函數最右邊的實參(一個或幾個)傳入。

舉個例子:

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"]

只有在傳兩個實參(匹配到 xy 形參)調用 f(..) 函數時,"z:last" 這個值才能被賦給函數的形參 z。在其他的例子里,不管左邊有多少個實參,"z:last" 都被傳給最右的實參。

一次傳一個

我們來看一個跟偏應用類似的技術,該技術將一個期望接收多個實參的函數拆解成連續的鏈式函數(chained functions),每個鏈式函數接收單一實參(實參個數:1)並返回另一個接收下一個實參的函數。

這就是柯里化(currying)技術。

首先,想象我們已創建了一個 ajax(..) 的柯里化版本。我們這樣使用它:

curriedAjax( "http://some.api/person" )
    ( { user: CURRENT_USER_ID } )
        ( function foundUser(user){ /* .. */ } );

我們將三次調用分別拆解開來,這也許有助於我們理解整個過程:

var personFetcher = curriedAjax( "http://some.api/person" );

var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } );

getCurrentUser( function foundUser(user){ /* .. */ } );

curriedAjax(..) 函數在每次調用中,一次只接收一個實參,而不是一次性接收所有實參(像 ajax(..) 那樣),也不是先傳部分實參再傳剩餘部分實參(藉助 partial(..) 函數)。

柯里化和偏應用相似,每個類似偏應用的連續柯里化調用都把另一個實參應用到原函數,一直到所有實參傳遞完畢。

不同之處在於,curriedAjax(..) 函數會明確地返回一個期望只接收下一個實參 data 的函數(我們把它叫做 curriedGetPerson(..)),而不是那個能接收所有剩餘實參的函數(像此前的 getPerson(..) 函數) 。

如果一個原函數期望接收 5 個實參,這個函數的柯里化形式只會接收第一個實參,並且返回一個用來接收第二個參數的函數。而這個被返回的函數又只接收第二個參數,並且返回一個接收第三個參數的函數。依此類推。

由此而知,柯里化將一個多參數(higher-arity)函數拆解為一系列的單元鏈式函數。

如何定義一個用來柯里化的實用函數呢?我們將要用到第 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 箭頭函數版本:

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 );
                }
            }
        )( [] );

此處的實現方式是把空數組 [] 當作 prevArgs 的初始實參集合,並且將每次接收到的 nextArgprevArgs 連接成 args 數組。當 args.length 小於 arity(原函數 fn(..) 被定義和期望的形參數量)時,返回另一個 curried(..) 函數(譯者註:這裡指代 nextCurried(..) 返回的函數)用來接收下一個 nextArg 實參,與此同時將 args 實參集合作為唯一的 prevArgs 參數傳入 nextCurried(..) 函數。一旦我們收集了足夠長度的 args 數組,就用這些實參觸發原函數 fn(..)

預設地,我們的實現方案基於下麵的條件:在拿到原函數期望的全部實參之前,我們能夠通過檢查將要被柯里化的函數的 length 屬性來得知柯里化需要迭代多少次。

假如你將該版本的 curry(..) 函數用在一個 length 屬性不明確的函數上 —— 函數的形參聲明包含預設形參值、形參解構,或者它是可變參數函數,用 ...args 當形參;參考第 2 章 —— 你將要傳入 arity 參數(作為 curry(..) 的第二個形參)來確保 curry(..) 函數的正常運行。

我們用 curry(..) 函數來實現此前的 ajax(..) 例子:

var curriedAjax = curry( ajax );

var personFetcher = curriedAjax( "http://some.api/person" );

var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } );

getCurrentUser( function foundUser(user){ /* .. */ } );

如上,我們每次函數調用都會新增一個實參,最終給原函數 ajax(..) 使用,直到收齊三個實參並執行 ajax(..) 函數為止。

還記得前面講到為數值列表的每個值加 3 的那個例子嗎?回顧一下,由於柯里化是和偏應用相似的,所以我們可以用幾乎相同的方式以柯里化來完成那個例子。

[1,2,3,4,5].map( curry( add )( 3 ) );
// [4,5,6,7,8]

partial(add,3)curry(add)(3) 兩者有什麼不同呢?為什麼你會選 curry(..) 而不是偏函數呢?當你先得知 add(..) 是將要被調整的函數,但如果這個時候並不能確定 3 這個值,柯里化可能會起作用:

var adder = curry( add );

// later
[1,2,3,4,5].map( adder( 3 ) );
// [4,5,6,7,8]

讓我們來看看另一個有關數字的例子,這次我們拿一個列表的數字做加法:

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 用來指定需要鏈式調用的次數)
var curriedSum = curry( sum, 5 );

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

這裡柯里化的好處是,每次函數調用傳入一個實參,並生成另一個特定性更強的函數,之後我們可以在程式中獲取並使用那個新函數。而偏應用則是預先指定所有將被偏應用的實參,產出一個等待接收剩下所有實參的函數。

如果想用偏應用來每次指定一個形參,你得在每個函數中逐次調用 partialApply(..) 函數。而被柯里化的函數可以自動完成這個工作,這讓一次單獨傳遞一個參數變得更加符合人機工程學。

在 JavaScript 中,柯里化和偏應用都使用閉包來保存實參,直到收齊所有實參後我們再執行原函數。

柯里化和偏應用有什麼用?

無論是柯里化風格(sum(1)(2)(3))還是偏應用風格(partial(sum,1,2)(3)),它們的簽名比普通函數簽名奇怪得多。那麼,在適應函數式編程的時候,我們為什麼要這麼做呢?答案有幾個方面。

首先是顯而易見的理由,使用柯里化和偏應用可以將指定分離實參的時機和地方獨立開來(遍及代碼的每一處),而傳統函數調用則需要預先確定所有實參。如果你在代碼某一處只獲取了部分實參,然後在另一處確定另一部分實參,這個時候柯里化和偏應用就能派上用場。

另一個最能體現柯里化應用的的是,當函數只有一個形參時,我們能夠比較容易地組合它們。因此,如果一個函數最終需要三個實參,那麼它被柯里化以後會變成需要三次調用,每次調用需要一個實參的函數。當我們組合函數時,這種單元函數的形式會讓我們處理起來更簡單。我們將在後面繼續探討這個話題。

如何柯里化多個實參?

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

具體來說,如果簡單看下柯里化在 Haskell 語言中的應用,我們會發現一個函數總是在一次柯里化調用中接收多個實參 —— 而不是接收一個包含多個值的元組(tuple,類似我們的數組)實參。

在 Haskell 中的示例:

foo 1 2 3

該示例調用了 foo 函數,並且根據傳入的三個值 123 得到了結果。但是在 Haskell 中,函數會自動被柯里化,這意味著我們傳入函數的值都分別傳入了單獨的柯里化調用。在 JS 中看起來則會是這樣:foo(1)(2)(3)。這和我此前講過的 curry(..) 風格如出一轍。

註意: 在 Haskell 中,foo (1,2,3) 不是把三個值當作單獨的實參一次性傳入函數,而是把它們包含在一個元組(類似 JS 數組)中作為單獨實參傳入函數。為了正常運行,我們需要改變 foo 函數來處理作為實參的元組。據我所知,在 Haskell 中我們沒有辦法在一次函數調用中將全部三個實參獨立地傳入,而需要柯里化調用每個函數。誠然,多次調用對於 Haskell 開發者來說是透明的,但對 JS 開發者來說,這在語法上更加一目瞭然。

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

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

具體來說,往往 JS 柯里化實用函數會允許你在每次柯里化調用中指定多個實參。回顧一下之前提到的 sum(..) 示例,鬆散柯里化應用會是下麵這樣:

var curriedSum = looseCurry( sum, 5 );

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

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

註意: 鬆散柯里化允許你傳入超過形參數量(arity,原函數確認或指定的形參數量)的實參。如果你將函數的參數設計成可配的或變化的,那麼鬆散柯里化將會有利於你。例如,如果你將要柯里化的函數接收 5 個實參,鬆散柯里化依然允許傳入超過 5 個的實參(curriedSum(1)(2,3,4)(5,6)),而嚴格柯里化就不支持 curriedSum(1)(2)(3)(4)(5)(6)

我們可以將之前的柯里化實現方式調整一下,使其適應這種常見的更鬆散的定義:

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 );
            }
        };
    })( [] );
}

現在每個柯里化調用可以接收一個或多個實參了(收集在 nextArgs 數組中)。至於這個實用函數的 ES6 箭頭函數版本,我們就留作一個小練習,有興趣的讀者可以模仿之前 curry(..) 函數的來完成。

反柯里化

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

不出意料的話,處理這個需求的標準實用函數通常被叫作 uncurry(..)。下麵是簡陋的實現方式:

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 箭頭函數形式
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 函數的行為完全一樣。雖然在某些庫中,反柯里化使函數變成和原函數(譯者註:這裡的原函數指柯里化之前的函數)類似的函數,但是凡事皆有例外,我們這裡就有一個例外。如果你傳入原函數期望數量的實參,那麼在反柯里化後,函數的行為(大多數情況下)和原函數相同。然而,如果你少傳了實參,就會得到一個仍然在等待傳入更多實參的部分柯里化函數。我們在下麵的代碼中說明這個怪異行為。

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() 函數最為常見的作用對象很可能並不是人為生成的柯里化函數(例如上文所示),而是某些操作所產生的已經被柯里化了的結果函數。我們將在本章後面關於 “無形參風格” 的討論中闡述這種應用場景。

只要一個實參

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

我們可以設計一個簡單的實用函數,它包裝一個函數調用,確保被包裝的函數只接收一個實參。既然實際上我們是強制把一個函數處理成單參數函數(unary),那我們索性就這樣命名實用函數:

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

// ES6 箭頭函數形式
var unary =
    fn =>
        arg =>
            fn( arg );

我們此前已經和 map(..) 函數打過照面了。它調用傳入其中的 mapping 函數時會傳入三個實參:valueindexlist。如果你希望你傳入 map(..) 的 mapping 函數只接收一個參數,比如 value,你可以使用 unary(..) 函數來操作:

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(..)` 修複後:
[1,2,3,4,5].map( unary( adder( 3 ) ) );
// [4,5,6,7,8]

另一種常用的 unary(..) 函數調用示例:

["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) 這個函數調用,如果 map(..) 函數調用它時在它的第二個實參位置傳入 index,那麼毫無疑問 parseInt(..) 會將 index 理解為 radix 參數,這是我們不希望發生的。而 unary(..) 函數創建了一個只接收第一個傳入實參,忽略其他實參的新函數,這就意味著傳入 index 不再會被誤解為 radix 參數。

傳一個返回一個

說到只傳一個實參的函數,在函數式編程工具庫中有另一種通用的基礎函數:該函數接收一個實參,然後什麼都不做,原封不動地返回實參值。

function identity(v) {
    return v;
}

// ES6 箭頭函數形式
var identity =
    v =>
        v;

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

舉個例子,想象一下你要用正則表達式拆分(split up)一個字元串,但輸出的數組中可能包含一些空值。我們可以使用 filter(..) 數組方法(下文會詳細說到這個方法)來篩除空值,而我們將 identity(..) 函數作為 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(..) 會簡單地返回傳入的值,而 JS 會將每個值強制轉換為 truefalse,這樣我們就能在最終的數組裡對每個值進行保存或排除。

小貼士: 像這個例子一樣,另外一個能被用作斷言的單實參函數是 JS 自有的 Boolean(..) 方法,該方法會強制把傳入值轉為 truefalse

另一個使用 identity(..) 的示例就是將其作為替代一個轉換函數(譯者註:transformation,這裡指的是對傳入值進行修改或調整,返回新值的函數)的預設函數:

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(..) 函數的 formatFn 參數設置預設值,我們可以叫出老朋友 partialRight(..) 函數:

var specialOutput = partialRight( output, upper );
var simpleOutput = partialRight( output, identity );

specialOutput( "Hello World" );     // HELLO WORLD
simpleOutput( "Hello World" );      // Hello World

你也可能會看到 identity(..) 被當作 map(..) 函數調用的預設轉換函數,或者作為某個函數數組的 reduce(..) 函數的初始值。我們將會在第 8 章中提到這兩個實用函數。

恆定參數

Certain API 禁止直接給方法傳值,而要求我們傳入一個函數,就算這個函數只是返回一個值。JS Promise 中的 then(..) 方法就是一個 Certain API。很多人聲稱 ES6 箭頭函數可以當作這個問題的 “解決方案”。但我這有一個函數式編程實用函數可以完美勝任該任務:

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

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

這個微小而簡潔的實用函數可以解決我們關於 then(..) 的煩惱:

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

// 對比:

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

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

擴展在參數中的妙用

在第 2 章中,我們簡要地講到了形參數組解構。回顧一下該示例:

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

foo( [1,2,3] );

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

當函數必須接收一個數組,而你卻想把數組內容當成單獨形參來處理的時候,這個技巧十分有用。

然而,有的時候,你無法改變原函數的定義,但想使用形參數組解構。舉個例子,請思考下麵的函數:

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

function bar(fn) {
    fn( [ 3, 9 ] );
}

bar( foo );         // 失敗

你註意到為什麼 bar(foo) 函數失敗了嗎?

我們將 [3,9] 數組作為單一值傳入 fn(..) 函數,但 foo(..) 期望接收單獨的 xy 形參。如果我們可以把 foo(..) 的函數聲明改變成 function foo([x,y]) { .. 那就好辦了。或者,我們可以改變 bar(..) 函數的行為,把調用改成 fn(...[3,9]),這樣就能將 39 分別傳入 foo(..) 函數了。

假設有兩個在此方法上互不相容的函數,而且由於各種原因你無法改變它們的聲明和定義。那麼你該如何一併使用它們呢?

為了調整一個函數,讓它能把接收的單一數組擴展成各自獨立的實參,我們可以定義一個輔助函數:

function spreadArgs(fn) {
    return function spreadFn(argsArr) {
        return fn( ...argsArr );
    };
}

// ES6 箭頭函數的形式:
var spreadArgs =
    fn =>
        argsArr =>
            fn( ...argsArr 

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 簡介 在SF上看到這樣一個提問: 如題,因為不得已的原因,需要寫若幹個全局函數。但又不想這樣: 題主問有什麼好的寫法? 解答: 如果你用 jQuery,你可以這樣寫 如果你不用 jQuery,可以直接實現類似的 extend: 在JavaScript中,命名空間可以幫助我們防止與全局命名空間下的其他 ...
  • 當我們在Spoil打算推出我們自己的移動端應用時,頭一個需要作出的決定就是:我們應該使用哪種編程語言?經過一番討論,我們最終做出的決定是:React-Native。學習一門新的“語言”或者框架並不是個大問題,但是老兄我得告訴你,React-Native和Redux確確實實是塊難啃的骨頭。這篇文章沒有... ...
  • Electron 可以讓你使用純 JavaScript 調用 Chrome 豐富的原生的介面來創造桌面應用。你可以把它看作一個專註於桌面應用的 Node.js 的變體,而不是 Web 伺服器。其基於瀏覽器的應用方式可以極方便的做各種響應式的交互,接下來介紹下關於 Electron 上衍生出的框架 N... ...
  • 找到build文件夾下麵的webpack.base.conf.js文件。 然後打開該文件,找到圖下這段代碼,把他註釋掉。 註釋掉之後,再進行子頁面等編寫的時候,空格不規範的情況下也不會再報錯啦。因為這個報錯對於初學者來說實在頭大。哈哈O(∩_∩)O哈哈~ 我標註的這些地方,原本是有嚴格的空格規範要求 ...
  • 不好意思,沒有像其他公眾號一樣趕著發文章,每年到這個時候總有一大波什麼今年前端預測,技術框架預測什麼的。我這次寫這篇文針對的對象,是想在今年踏入前端這行的人們,不管你現在是徘徊在門口,還是已經半隻腳踏入這片未知領域,都可以參考一下先行者的經驗。 ...
  • 這是一本可供任何人使用的指南,用於學習前端開發實踐。該指南大體上勾勒出了前端工程的輪廓,同時也討論了前端工程的實踐:2017 年,如何學習前端工程,用什麼工具來實踐? 筆者有意將本書打造為一份專業資料,為想要或正在實踐的前端開發者們提供學習材料和開發工具。其次,它同樣可供主管、CTO、講師和獵頭們... ...
  • prop()方法和attr()類似,但是HTML5規定有一種屬性在DOM節點中可以沒有值,只有出現與不出現兩種,例如: attr()和prop()對於屬性checked處理有所不同: prop()返回值更合理一些。不過,用is()方法判斷更好: 類似的屬性還有selected,處理時最好用is(': ...
  • 從2015年2月轉行進入IT行業,到現在也有將近兩年的時間了,從最開始的java到現在的前端,前進的路上一直靠自己摸索,一路走到現在,前端大神是絕對談不上的,最多算一隻剛入門的菜鳥。 從最開始的懵懵懂懂,到現在學著開始寫github、寫博客,其實技術上沒有太多可寫的,畢竟自己也才剛剛入門,只能說是按 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...