輕量函數(shù)式 JavaScript 第二章:函數(shù)式函數(shù)的基礎(chǔ)

感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取

函數(shù)式編程 不是使用 function 關(guān)鍵字編程。 如果它真有那么簡(jiǎn)單,我在這里就可以結(jié)束這本書(shū)了!但重要的是,函數(shù)確實(shí)是 FP 的中心。使我們的代碼成為 函數(shù)式 的,是我們?nèi)绾问褂煤瘮?shù)。

但是,你確信你知道 函數(shù) 是什么意思嗎?

在這一章中,我們將要通過(guò)講解函數(shù)的所有基本方面來(lái)為本書(shū)的剩余部分打下基礎(chǔ)。在某種意義上,這里的內(nèi)容即便是對(duì)非 FP 程序員來(lái)說(shuō)也是應(yīng)當(dāng)知道的關(guān)于函數(shù)的一切。但是如果我們想要從 FP 的概念中學(xué)到竟可能多的東西,我們必須 知道 函數(shù)的里里外外。

振作起來(lái),關(guān)于函數(shù)東西可能比你已經(jīng)知道的東西多得多。

什么是函數(shù)?

要解釋函數(shù)式編程,我所能想到的最自然的起點(diǎn)就是 函數(shù)。這看起來(lái)再明顯不過(guò)了,但我想我們的旅程需要堅(jiān)實(shí)的第一步。

那么……什么是函數(shù)?

數(shù)學(xué)簡(jiǎn)憶

我知道我承諾過(guò)盡可能遠(yuǎn)離數(shù)學(xué),但稍稍忍耐我片刻,在繼續(xù)之前我們快速地檢視一些東西:代數(shù)中有關(guān)函數(shù)和圖像的基礎(chǔ)。

你還記得在學(xué)校里學(xué)過(guò)的關(guān)于 f(x) 的一些東西嗎?等式 y = f(x) 呢?

比如說(shuō)一個(gè)等式這樣定義的:<code>f(x) = 2x2 + 3</code>。這是什么意思?給這個(gè)函數(shù)畫(huà)出圖像是什么意思?這就是圖像:

你能注意到,對(duì)于任何 x 的值,比如 2,如果你將它插入這個(gè)等式,你會(huì)得到 11。那么 11 是什么?它是函數(shù) f(x)返回值,代表我們剛才說(shuō)到的 y 值。

換句話說(shuō),在圖像的曲線上有一個(gè)點(diǎn) (2,11)。而且對(duì)于我們插入的任意的 x 的值,我們都能得到另一個(gè)與之相對(duì)應(yīng)的 y 值作為一個(gè)點(diǎn)的坐標(biāo)。比如另外一個(gè)點(diǎn) (0,3),以及另一個(gè)點(diǎn) (-1,5)。將這些點(diǎn)放在一起,你就得到了上面的拋物線圖像。

那么這到底與 FP 有什么關(guān)系?

在數(shù)學(xué)中,一個(gè)函數(shù)總是接受輸入,并且總是給出輸出。一個(gè)你將經(jīng)常聽(tīng)到的 FP 術(shù)語(yǔ)是“態(tài)射(morphism)”;這個(gè)很炫的詞用來(lái)描述一個(gè)值的集合映射到另一個(gè)值的集合,就像一個(gè)函數(shù)的輸入與這個(gè)函數(shù)的輸入的關(guān)系一樣。

在代數(shù)中,這些輸入與輸出經(jīng)常被翻譯為被繪制的圖像的坐標(biāo)的一部分。然而,我們我可以使用各種各樣的輸入與輸出定義函數(shù),而且它們不必與視覺(jué)上圖像的曲線有任何關(guān)系。

函數(shù) vs 過(guò)程

那么為什么說(shuō)了半天數(shù)學(xué)和圖像?因?yàn)樵谀撤N意義上,函數(shù)式編程就是以這種數(shù)學(xué)意義上的 函數(shù) 來(lái)使用函數(shù)。

你可能更習(xí)慣于將函數(shù)考慮為過(guò)程(procedures)。它有什么區(qū)別?一個(gè)任意功能的集合。它可能有輸入,也可能沒(méi)有。它可能有一個(gè)輸出(return 值),也可能沒(méi)有。

而一個(gè)函數(shù)接收輸入并且絕對(duì)總是有一個(gè) return 值。

如果你打算進(jìn)行函數(shù)式編程,你就應(yīng)當(dāng)盡可能多地使用函數(shù),而不是過(guò)程。你所有的 function 都應(yīng)當(dāng)接收輸入并返回輸出。為什么?這個(gè)問(wèn)題的答案有許多層次的含義,我們將在這本書(shū)中逐一揭示它們。

函數(shù)輸入

根據(jù)這個(gè)定義,所有函數(shù)都需要輸入。

你有時(shí)會(huì)聽(tīng)到人們稱(chēng)它們?yōu)椤皩?shí)際參數(shù)(arguments)”,而有時(shí)稱(chēng)為“形式參數(shù)(parameters)”。那么這都是什么意思?

實(shí)際參數(shù) 是你傳入的值,而 形式參數(shù) 在函數(shù)內(nèi)部被命名的變量,它們接收那些被傳入的值。例如:

function foo(x,y) {
    // ..
}

var a = 3;

foo( a, a * 2 );

aa * 2(實(shí)際上,是這個(gè)表達(dá)式的值,6) 是 foo(..) 調(diào)用的 實(shí)際參數(shù)xy 是接收實(shí)際參數(shù)值(分別是 36)的 形式參數(shù)

注意: 在 JavaScript 中,不要求 實(shí)際參數(shù) 的數(shù)量與 形式參數(shù) 的數(shù)量相吻合。如果你傳入的 實(shí)際參數(shù) 多于被聲明來(lái)接受它們的 形式參數(shù) ,那么這些值會(huì)原封不動(dòng)地被傳入。這些值可以用幾種不同的方式訪問(wèn),包括老舊的 arguments 對(duì)象。如果你傳入的 實(shí)際參數(shù) 少于被聲明的 形式參數(shù),那么每一個(gè)無(wú)人認(rèn)領(lǐng)的形式參數(shù)都是一個(gè) “undefined” 值,這意味著它在這個(gè)函數(shù)的作用域中存在而且可用,只是初始值是空的 undefined

輸入計(jì)數(shù)

被“期待”的實(shí)際參數(shù)的數(shù)量 —— 你可能想向它傳遞多少實(shí)際參數(shù) —— 是由被聲明的形式參數(shù)的數(shù)量決定的。

function foo(x,y,z) {
    // ..
}

foo(..) 期待 三個(gè)實(shí)際參數(shù),因?yàn)樗鼡碛腥齻€(gè)被聲明的形式參數(shù)。這個(gè)數(shù)量有一個(gè)特殊的術(shù)語(yǔ):元(arity)。元是函數(shù)聲明中形式參數(shù)的數(shù)量。foo(..) 的元是 3

你可能會(huì)想在運(yùn)行時(shí)期間檢查一個(gè)函數(shù)引用來(lái)判定它的元。這可以通過(guò)這個(gè)函數(shù)引用的 length 屬性來(lái)完成:

function foo(x,y,z) {
    // ..
}

foo.length;             // 3

一個(gè)在執(zhí)行期間判定元的原因可能是,一段代碼從多個(gè)源頭接受一個(gè)函數(shù)引用,并且根據(jù)每個(gè)函數(shù)引用的元來(lái)發(fā)送不同的值。

例如,想象這樣一種情況,一個(gè)函數(shù)引用 fn 可能期待一個(gè),兩個(gè),或三個(gè)實(shí)際參數(shù),但你總是想要在最后一個(gè)位置上傳遞變量 x

// `fn` 被設(shè)置為某個(gè)函數(shù)的引用
// `x` 存在并擁有一些值

if (fn.length == 1) {
    fn( x );
}
else if (fn.length == 2) {
    fn( undefined, x );
}
else if (fn.length == 3) {
    fn( undefined, undefined, x );
}

提示: 一個(gè)函數(shù)的 length 屬性是只讀的,而且它在你聲明這個(gè)函數(shù)時(shí)就已經(jīng)被決定了。它應(yīng)當(dāng)被認(rèn)為實(shí)質(zhì)上是一種元數(shù)據(jù),用來(lái)描述這個(gè)函數(shù)意料之中的用法。

一個(gè)要小心的坑是,特定種類(lèi)的形式參數(shù)列表可以使函數(shù)的 length 屬性報(bào)告的東西與你期待的不同。不要擔(dān)心,我們會(huì)在本章稍后講解每一種(ES6 引入的)特性:

function foo(x,y = 2) {
    // ..
}

function bar(x,...args) {
    // ..
}

function baz( {a,b} ) {
    // ..
}

foo.length;             // 1
bar.length;             // 1
baz.length;             // 1

如果你使用這些形式參數(shù)中的任意一種,那么要小心你函數(shù)的 length 值可能會(huì)使你驚訝。

那么如何計(jì)數(shù)當(dāng)前函數(shù)調(diào)用收到的實(shí)際參數(shù)數(shù)量呢?這曾經(jīng)是小菜一碟,但現(xiàn)在情況變得稍微復(fù)雜一些。每個(gè)函數(shù)都有一個(gè)可以使用的 arguments (類(lèi)數(shù)組)對(duì)象,它持有每個(gè)被傳入的實(shí)際參數(shù)的引用。你可檢查 argumentslength 屬性來(lái)搞清楚有多少參數(shù)被實(shí)際傳遞了:

function foo(x,y,z) {
    console.log( arguments.length );    // 2
}

foo( 3, 4 );

在 ES5(具體地說(shuō),strict 模式)中,arguments 被認(rèn)為是有些軟廢棄了;許多人都盡量避免使用它。它永遠(yuǎn)都不會(huì)被移除 —— 在 JS 中,不論那將會(huì)變得多么方便,我們“永遠(yuǎn)”都不會(huì)破壞向下的兼容性 —— 但是由于種種原因依然強(qiáng)烈建議你盡可能避免使用它。

然而,我建議 arguments.length,而且僅有它,在你需要關(guān)心被傳入的實(shí)際參數(shù)的數(shù)量時(shí)是可以繼續(xù)使用的。某個(gè)未來(lái)版本的 JS 中有可能會(huì)加入一個(gè)特性,在沒(méi)有 arguments.length 的情況下恢復(fù)判定被傳遞的實(shí)際參數(shù)數(shù)量的能力;如果這真的發(fā)生了,那么我們就可以完全放棄 arguments 的使用了。

小心:絕不要 按位置訪問(wèn)實(shí)際參數(shù),比如 arguments[1]。如果你必須這么做的話,堅(jiān)持只使用 arguments.length

除非……你如何訪問(wèn)一個(gè)在超出被聲明的形式參數(shù)位置上傳入的實(shí)際參數(shù)?我一會(huì)就會(huì)回答這個(gè)問(wèn)題;但首先,退一步問(wèn)你自己,“為什么我想要這么做?”。把這個(gè)問(wèn)題認(rèn)真地考慮幾分鐘。

這種情況的發(fā)生應(yīng)該非常少見(jiàn);它不應(yīng)當(dāng)是你通常所期望的,或者在你編寫(xiě)函數(shù)時(shí)所依靠的東西。如果你發(fā)現(xiàn)自己身陷于此,那么就再多花20分鐘,試著用一種不同的方式來(lái)設(shè)計(jì)這個(gè)函數(shù)的交互。即使這個(gè)參數(shù)是特殊的,也給它起個(gè)名字。

一個(gè)接收不確定數(shù)量的實(shí)際參數(shù)的函數(shù)簽名稱(chēng)為可變參函數(shù)(variadic function)。有些人喜歡這種風(fēng)格的函數(shù)設(shè)計(jì),但我想你將會(huì)發(fā)現(xiàn) FP 程序員經(jīng)常想要盡量避免這些。

好了,在這一點(diǎn)上嘮叨得夠多了。

假定你需要以一種類(lèi)似數(shù)組下標(biāo)定位的方式來(lái)訪問(wèn)實(shí)際參數(shù),這可能是因?yàn)槟阏谠L問(wèn)一個(gè)沒(méi)有正式形式參數(shù)位置的實(shí)際參數(shù)。我們?cè)撊绾巫觯?/p>

ES6 前來(lái)拯救!讓我們使用 ... 操作符來(lái)聲明我們的函數(shù) —— 它有多個(gè)名稱(chēng):“擴(kuò)散”、“剩余”、或者(我最喜歡的)“聚集”。

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

看到形式參數(shù)列表中的 ...args 了嗎?這是一種新的 ES6 聲明形式,它告訴引擎去收集(嗯哼,聚集)所有剩余的(如果有的話)沒(méi)被賦值給命名形式參數(shù)的實(shí)際參數(shù),并將它們放到名為 args 的真正的數(shù)組中。args 將總是一個(gè)數(shù)組,即便是空的。但它 不會(huì) 包含那些已經(jīng)賦值給形式參數(shù) xy、和 z 的值,只有超過(guò)前三個(gè)值被傳入的所有東西。

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

foo();                  // undefined undefined undefined []
foo( 1, 2, 3 );         // 1 2 3 []
foo( 1, 2, 3, 4 );      // 1 2 3 [ 4 ]
foo( 1, 2, 3, 4, 5 );   // 1 2 3 [ 4, 5 ]

所以,如果你 真的 想要設(shè)計(jì)一個(gè)解析任意多實(shí)際參數(shù)的函數(shù),就在末尾使用 ...args(或你喜歡的其他任何名字)。現(xiàn)在,你將得到一個(gè)真正的,沒(méi)有被廢棄的,不討人嫌的數(shù)組來(lái)訪問(wèn)那些實(shí)際參數(shù)。

只不過(guò)要注意,值 4 在這個(gè) args 的位置 0 上,而不是位置 3。而且它的 length 值將不會(huì)包括 12、和 3 這三個(gè)值。...args 聚集所有其余的東西,不包含 xy、和 z

你甚至 可以 在沒(méi)有聲明任何正式形式參數(shù)的參數(shù)列表中使用 ... 操作符:

function foo(...args) {
    // ..
}

無(wú)論實(shí)際參數(shù)是什么,args 現(xiàn)在都是一個(gè)完全的實(shí)際參數(shù)的數(shù)組,而且你可以使用 args.length 來(lái)知道究竟有多少個(gè)實(shí)際參數(shù)被傳入了。而且如果你選擇這樣做的話,你可以安全地使用 args[1]args[317]。但是,拜托不要傳入318個(gè)實(shí)際參數(shù)。

說(shuō)到 ES6 的好處,關(guān)于你函數(shù)的實(shí)際參數(shù)與形式參數(shù),還有幾種你可能想知道的其他的技巧。這個(gè)簡(jiǎn)要概覽之外的更多信息,參見(jiàn)我的 “你不懂 JS —— ES6 與未來(lái)” 的第二章。

實(shí)際參數(shù)技巧

要是你想要傳遞一個(gè)值的數(shù)組作為你函數(shù)調(diào)用的實(shí)際參數(shù)呢?

function foo(...args) {
    console.log( args[3] );
}

var arr = [ 1, 2, 3, 4, 5 ];

foo( ...arr );                      // 4

我們使用了我們的新朋友 ...,它不只是在形式參數(shù)列表中可以使用;而且還可以在調(diào)用點(diǎn)的實(shí)際參數(shù)列表中使用。在這樣的上下文環(huán)境中它將擁有相反的行為。在形式參數(shù)列表中,我們說(shuō)它將實(shí)際參數(shù) 聚集 在一起。在實(shí)際參數(shù)列表中,它將它們 擴(kuò)散 開(kāi)來(lái)。所以 arr 的內(nèi)容實(shí)際上被擴(kuò)散為 foo(..) 調(diào)用的各個(gè)獨(dú)立的實(shí)際參數(shù)。你能看出這與僅僅傳入 arr 數(shù)組的整個(gè)引用有什么不同嗎?

順帶一提,多個(gè)值與 ... 擴(kuò)散是可以穿插的,只要你認(rèn)為合適:

var arr = [ 2 ];

foo( 1, ...arr, 3, ...[4,5] );      // 4

以這種對(duì)稱(chēng)的感覺(jué)考慮 ...:在一個(gè)值的列表的位置,它 擴(kuò)散。在一個(gè)賦值的位置 —— 比如形式參數(shù)列表,因?yàn)閷?shí)際參數(shù)被 賦值給 了形式參數(shù) —— 它 聚集

不管你調(diào)用哪一種行為,... 都令使用實(shí)際參數(shù)列表變得非常簡(jiǎn)單。用slice(..)concat(..)apply(..) 來(lái)倒騰我們實(shí)際參數(shù)值數(shù)組的日子一去不復(fù)返了。

形式參數(shù)技巧

在 ES6 中, 形式參數(shù)可以被聲明 默認(rèn)值。在這個(gè)形式參數(shù)的實(shí)際參數(shù)沒(méi)有被傳遞,或者被傳遞了一個(gè) undefined 值的情況下,默認(rèn)的賦值表達(dá)式將會(huì)取而代之。

考慮如下代碼:

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

foo();                  // 3
foo( undefined );       // 3
foo( null );            // null
foo( 0 );               // 0

注意: 我們不會(huì)在此涵蓋更多的細(xì)節(jié),但是默認(rèn)值表達(dá)式是懶惰的,這意味著除非需要它不會(huì)被求值。另外,它可以使用任意合法的 JS 表達(dá)式,甚至是一個(gè)函數(shù)調(diào)用。這種能力使得許多很酷的技巧成為可能。例如,你可以在形式參數(shù)列表中聲明 x = required(),而在 required() 函數(shù)中簡(jiǎn)單地 throw "This argument is required.",來(lái)確保其他人總是帶著指定的實(shí)際/形式參數(shù)來(lái)調(diào)用你的函數(shù)。

另一個(gè)我們可以在形式參數(shù)列表中使用的技巧稱(chēng)為 “解構(gòu)”。我們將簡(jiǎn)要地掃它一眼,因?yàn)檫@個(gè)話題要比我們?cè)谶@里討論的復(fù)雜太多了。同樣,更多信息參考我的 “ES6 與未來(lái)”。

還記得剛才可以接收318個(gè)實(shí)際參數(shù)的 foo(..) 嗎!?

function foo(...args) {
    // ..
}

foo( ...[1,2,3] );

要是我們想改變這種互動(dòng)方式,讓我們函數(shù)的調(diào)用方傳入一個(gè)值的數(shù)組而非各個(gè)獨(dú)立的實(shí)際參數(shù)值呢?只要去掉這兩個(gè) ... 就好:

function foo(args) {
    // ..
}

foo( [1,2,3] );

這很簡(jiǎn)單。但如果我們想給被傳入的數(shù)組的前兩個(gè)值賦予形式參數(shù)名呢?我們不再聲明獨(dú)立的形式參數(shù)了,看起來(lái)我們失去了這種能力。但解構(gòu)就是答案:

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

foo( [1,2,3] );

你發(fā)現(xiàn)現(xiàn)在形式參數(shù)列表周?chē)姆嚼ㄌ?hào) [ .. ] 了嗎?這就是數(shù)組解構(gòu)。解構(gòu)為你想看到的某種結(jié)構(gòu)(對(duì)象,數(shù)組等)聲明了一個(gè) 范例,描述應(yīng)當(dāng)如何將它分解(分配)為各個(gè)獨(dú)立的部分。

在這個(gè)例子中,解構(gòu)告訴引擎在這個(gè)賦值的位置(也就是形式參數(shù))上期待一個(gè)數(shù)組。范例中說(shuō)將這個(gè)數(shù)組的第一個(gè)值賦值給稱(chēng)為 x 的本地形式參數(shù)變量,第二個(gè)賦值給 y,而剩下的所有東西都 聚集args 中。

你本可以像下面這樣手動(dòng)地做同樣的事情:

function foo(params) {
    var x = params[0];
    var y = params[1];
    var args = params.slice( 2 );

    // ..
}

但是現(xiàn)在我們要揭示一個(gè)原則 —— 我們將在本文中回顧它許多許多次 —— 的第一點(diǎn):聲明式代碼經(jīng)常要比指令式代碼表意更清晰。

聲明式代碼,就像前面代碼段中的解構(gòu),關(guān)注于一段代碼的結(jié)果應(yīng)當(dāng)是什么樣子。指令式代碼,就像剛剛展示的手動(dòng)賦值,關(guān)注于如何得到結(jié)果。如果稍后再讀這段代碼,你就不得不在大腦中執(zhí)行它來(lái)得到期望的結(jié)果。它的結(jié)果被 編碼 在這里,但不清晰。

不論什么地方,也不論我們的語(yǔ)言或庫(kù)/框架允許我們這樣做到多深的程度,我們都應(yīng)當(dāng)努力使用聲明式的、自解釋的代碼。

正如我們可以解構(gòu)數(shù)組,我們還可以解構(gòu)對(duì)象形式參數(shù):

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

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

我們將一個(gè)對(duì)象作為實(shí)際參數(shù)傳入,它被解構(gòu)為兩個(gè)分離的形式參數(shù)變量 xy,被傳入的對(duì)象中具有相應(yīng)屬性名稱(chēng)的值將會(huì)被賦予這兩個(gè)變量。對(duì)象中不存在 x 屬性并不要緊;它會(huì)如你所想地那樣得到一個(gè) undefined 變量。

但是在這個(gè)形式參數(shù)對(duì)象解構(gòu)中我想讓你關(guān)注的是被傳入 foo(..) 的對(duì)象。

foo(undefined,3) 這樣普通的調(diào)用點(diǎn),位置用于將實(shí)際參數(shù)映射到形式參數(shù)上;我們將 3 放在第二個(gè)位置上使它被賦值給形式參數(shù) y。但是在這種引入了形式參數(shù)解構(gòu)的新型調(diào)用點(diǎn)中,一個(gè)簡(jiǎn)單的對(duì)象-屬性指示了哪個(gè)形式參數(shù)應(yīng)該被賦予實(shí)際參數(shù)值 3

我們不必在這個(gè)調(diào)用點(diǎn)中說(shuō)明 x,因?yàn)槲覀儗?shí)際上不關(guān)心 x。我們只是忽略它,而不是必須去做傳入 undefined 作為占位符這樣令人分心的事情。

有些語(yǔ)言直接擁有這種行為特性:命名實(shí)際參數(shù)。換句話說(shuō),在調(diào)用點(diǎn)中,給一個(gè)輸入值打上一個(gè)標(biāo)簽來(lái)指示它映射到哪個(gè)形式參數(shù)上。JavaScript 不具備命名實(shí)際參數(shù),但是形式參數(shù)對(duì)象解構(gòu)是最佳后備選項(xiàng)。

使用對(duì)象解構(gòu)傳入潛在的多個(gè)實(shí)際參數(shù) —— 這樣做的一個(gè)與 FP 關(guān)聯(lián)的好處是,只接收單一形式參數(shù)(那個(gè)對(duì)象)的函數(shù)與另一個(gè)函數(shù)的單一輸出組合起來(lái)要容易得多。稍后會(huì)詳細(xì)講解這一點(diǎn)。

回想一下,“元”這個(gè)術(shù)語(yǔ)指一個(gè)函數(shù)期待接收多少形式參數(shù)。一個(gè)元為 1 的函數(shù)也被稱(chēng)為一元函數(shù)。在 FP 中,我們將盡可能使我們的函數(shù)是一元的,而且有時(shí)我們甚至?xí)褂酶鞣N函數(shù)式技巧將一個(gè)高元函數(shù)轉(zhuǎn)換為一個(gè)一元的形式。

注意: 在第三章中,我們將重溫這種命名實(shí)際參數(shù)解構(gòu)技巧,來(lái)對(duì)付惱人的形式參數(shù)順序問(wèn)題。

根據(jù)輸入變化的函數(shù)

考慮這個(gè)函數(shù):

function foo(x,y) {
    if (typeof x == "number" && typeof y == "number") {
        return x * y;
    }
    else {
        return x + y;
    }
}

顯然,這個(gè)造作的例子會(huì)根據(jù)你傳入的輸入不同而表現(xiàn)出不同的行為。

例如:

foo( 3, 4 );            // 12

foo( "3", 4 );          // "34"

程序員們像這樣定義函數(shù)的原因之一,是可以更方便地將不同的行為 重載(overload) 入一個(gè)函數(shù)中。最廣為人知的例子就是由許多像 JQuery 這樣的主流庫(kù)提供的 $(..) 函數(shù)。根據(jù)你向它傳遞什么實(shí)際參數(shù),這個(gè)“錢(qián)號(hào)”函數(shù)大概擁有十幾種非常不同的行為 —— 從 DOM 元素查詢(xún)到 DOM 元素創(chuàng)建,以及將一個(gè)函數(shù)拖延到 DOMContentLoaded 事件之后。

感覺(jué)這種方式有一種優(yōu)勢(shì),就是需要學(xué)習(xí)的 API 少一些(只有一個(gè) $(..) 函數(shù)),但是在代碼可讀性上具有明顯的缺陷,而且不得不小心地檢查到底什么東西被傳入了,才能解讀一個(gè)調(diào)用要做什么。

這種基于一個(gè)函數(shù)的輸入來(lái)重載許多不同行為的技術(shù)稱(chēng)為特設(shè)多態(tài)(ad hoc polymorphism)。

這種設(shè)計(jì)模式的另一種表現(xiàn)形式是,使一個(gè)函數(shù)在不同場(chǎng)景下?lián)碛胁煌妮敵觯ǜ嗉?xì)節(jié)參加下一節(jié))。

警告: 要對(duì)這里的 方便 的沖動(dòng)特別小心。僅僅因?yàn)槟憧梢赃@樣設(shè)計(jì)一個(gè)函數(shù),而且即便可能立即感知到一些好處,這種設(shè)計(jì)決定所帶來(lái)的長(zhǎng)期成本也可能不令人愉快。

函數(shù)輸出

在 JavaScript 中,函數(shù)總是返回一個(gè)值。這三個(gè)函數(shù)都擁有完全相同的 return 行為:

function foo() {}

function bar() {
    return;
}

function baz() {
    return undefined;
}

如果你沒(méi)有 return 或者你僅僅有一個(gè)空的 return;,那么 undefined 值就會(huì)被隱含地 return

但是要盡可能地保持 FP 中函數(shù)定義的精神 —— 使用函數(shù)而不是過(guò)程 —— 我們的函數(shù)應(yīng)當(dāng)總是擁有輸出,這意味著它們應(yīng)當(dāng)明確地 return 一個(gè)值,而且通常不是 undefined

一個(gè) return 語(yǔ)句只能返回一個(gè)單一的值。所以如果你的函數(shù)需要返回多個(gè)值,你唯一可行的選項(xiàng)是將它們收集到一個(gè)像數(shù)組或?qū)ο筮@樣的復(fù)合值中:

function foo() {
    var retValue1 = 11;
    var retValue2 = 31;
    return [ retValue1, retValue2 ];
}

就像解構(gòu)允許我們?cè)谛问絽?shù)中拆分?jǐn)?shù)組/對(duì)象一樣,我們也可以在普通的賦值中這么做:

function foo() {
    var retValue1 = 11;
    var retValue2 = 31;
    return [ retValue1, retValue2 ];
}

var [ x, y ] = foo();
console.log( x + y );           // 42

將多個(gè)值收集到一個(gè)數(shù)組(或?qū)ο螅┲蟹祷兀^而將這些值解構(gòu)回獨(dú)立的賦值,對(duì)于函數(shù)來(lái)說(shuō)是一種透明地表達(dá)多個(gè)輸出的方法。

提示: 如果我沒(méi)有這么提醒你,那將是我的疏忽:花點(diǎn)時(shí)間考慮一下,一個(gè)需要多個(gè)輸出的函數(shù)是否能夠被重構(gòu)來(lái)避免這種情況,也許分成兩個(gè)或更多更小的意圖單一的函數(shù)?有時(shí)候這是可能的,有時(shí)候不;但你至少應(yīng)該考慮一下。

提前返回

return 語(yǔ)句不僅是從一個(gè)函數(shù)中返回一個(gè)值。它還是一種流程控制結(jié)構(gòu);它會(huì)在那一點(diǎn)終止函數(shù)的運(yùn)行。因此一個(gè)帶有多個(gè) return 語(yǔ)句的函數(shù)就擁有多個(gè)可能的出口,如果有許多路徑可以產(chǎn)生輸出,那么這就意味著閱讀一個(gè)函數(shù)來(lái)理解它的輸出行為可能更加困難。

考慮如下代碼:

function foo(x) {
    if (x > 10) return x + 1;

    var y = x / 2;

    if (y > 3) {
        if (x % 2 == 0) return x;
    }

    if (y > 1) return y;

    return x;
}

突擊測(cè)驗(yàn):不使用瀏覽器運(yùn)行這段代碼,foo(2) 返回什么?foo(4) 呢?foo(8) 呢?foo(12) 呢?

你對(duì)自己的答案有多自信?你為這些答案交了多少智商稅?我考慮它時(shí),前兩次都錯(cuò)了,而且我是用寫(xiě)的!

我認(rèn)為這里的一部分可讀性問(wèn)題是,我們不僅將 return 用于返回不同的值,而且還將它作為一種流程控制結(jié)構(gòu),在特定的情況下提前退出函數(shù)的執(zhí)行。當(dāng)然有更好的方式編寫(xiě)這種流程控制(例如 if 邏輯),但我也認(rèn)為有辦法使輸出的路徑更加明顯。

注意: 突擊測(cè)驗(yàn)的答案是 228、和 13.

考慮一下這個(gè)版本的代碼:

function foo(x) {
    var retValue;

    if (retValue == undefined && x > 10) {
        retValue = x + 1;
    }

    var y = x / 2;

    if (y > 3) {
        if (retValue == undefined && x % 2 == 0) {
            retValue = x;
        }
    }

    if (retValue == undefined && y > 1) {
        retValue = y;
    }

    if (retValue == undefined) {
        retValue = x;
    }

    return retValue;
}

這個(gè)版本無(wú)疑更加繁冗。但我要爭(zhēng)辯的是它的邏輯追溯起來(lái)更簡(jiǎn)單,因?yàn)槊恳粋€(gè) retValue 可能被設(shè)置的分支都被一個(gè)檢查它是否已經(jīng)被設(shè)置過(guò)的條件 守護(hù) 著。

我們沒(méi)有提前從函數(shù)中 return 出來(lái),而是使用了普通的流程控制來(lái)決定 retValue 的賦值。最后,我們單純地 return retValue

我并不是在無(wú)條件地宣稱(chēng)你應(yīng)當(dāng)總是擁有一個(gè)單獨(dú)的 return,或者你絕不應(yīng)該提早 return,但我確實(shí)認(rèn)為你應(yīng)該對(duì) return 在你的函數(shù)定義中制造隱晦流程控制的部分多加小心。試著找出表達(dá)邏輯的最明確的方式;那通常是最好的方式。

沒(méi)有被 return 的輸出

你可能在你寫(xiě)過(guò)的大部分代碼中用過(guò),但可能沒(méi)有太多考慮過(guò)的技術(shù)之一,就是通過(guò)簡(jiǎn)單地改變函數(shù)外部的變量來(lái)使它輸出一些或全部的值。

記得我們?cè)诒菊略缦鹊?<code>f(x) = 2x2 + 3</code> 函數(shù)嗎?我們可以用 JS 這樣定義它:

var y;

function foo(x) {
    y = (2 * Math.pow( x, 2 )) + 3;
}

foo( 2 );

y;                      // 11

我知道這是一個(gè)愚蠢的例子;我們本可以簡(jiǎn)單地 return 值,而非在函數(shù)內(nèi)部將它設(shè)置在 y 中:

function foo(x) {
    return (2 * Math.pow( x, 2 )) + 3;
}

var y = foo( 2 );

y;                      // 11

兩個(gè)函數(shù)都完成相同的任務(wù)。我們有任何理由擇優(yōu)選用其中之一嗎?有,絕對(duì)有。

一個(gè)解釋它們的不同之處的方式是,第二個(gè)版本中的 return 標(biāo)明了一個(gè)明確的輸出,而前者中的 y 賦值是一種隱含的輸出。此時(shí)你能已經(jīng)有了某種指引你的直覺(jué);通常,開(kāi)發(fā)者們優(yōu)先使用明確的模式,而非隱含的。

但是改變外部作用域中的變量,就像我們?cè)?foo(..) 內(nèi)部中對(duì) y 賦值所做的,只是得到隱含輸出的方式之一。一個(gè)更微妙的例子是通過(guò)引用來(lái)改變非本地值。

考慮如下代碼:

function sum(list) {
    var total = 0;
    for (let i = 0; i < list.length; i++) {
        if (!list[i]) list[i] = 0;

        total = total + list[i];
    }

    return total;
}

var nums = [ 1, 3, 9, 27, , 84 ];

sum( nums );            // 124

這個(gè)函數(shù)最明顯的輸出是我們明確地 return 的和 124。但你發(fā)現(xiàn)其他的輸出了嗎?試著運(yùn)行這代碼然后檢查 nums 數(shù)組。現(xiàn)在你發(fā)現(xiàn)不同了嗎?

現(xiàn)在在位置 4 上取代 undefined 空槽值的是一個(gè) 0。看起來(lái)無(wú)害的 list[i] = 0 操作影響了外部的數(shù)組值,即便我們操作的是本地形式參數(shù)變量 list

為什么?因?yàn)?list 持有一個(gè) nums 引用的引用拷貝,而不是數(shù)組值 [1,3,9,..] 的值拷貝。因?yàn)?JS 對(duì)數(shù)組,對(duì)象,以及函數(shù)使用引用和引用拷貝,所以我們可以很容易地從我們的函數(shù)中制造輸出,這甚至是偶然的。

這種隱含的函數(shù)輸出在 FP 世界中有一個(gè)特殊名稱(chēng):副作用(side effects)。而一個(gè) 沒(méi)有副作用 的函數(shù)也有一個(gè)特殊名稱(chēng):純函數(shù)(pure function)。在后面的章節(jié)中我們將更多地討論這些內(nèi)容,但要點(diǎn)是,我們將盡一切可能優(yōu)先使用純函數(shù)并避免副作用。

函數(shù)的函數(shù)

函數(shù)可以接收并返回任意類(lèi)型的值。一個(gè)接收或返回一個(gè)或多個(gè)其他函數(shù)的函數(shù)有一個(gè)特殊的名稱(chēng):高階函數(shù)(higher-order function)。

考慮如下代碼:

function forEach(list,fn) {
    for (let i = 0; i < list.length; i++) {
        fn( list[i] );
    }
}

forEach( [1,2,3,4,5], function each(val){
    console.log( val );
} );
// 1 2 3 4 5

forEach(..) 是一個(gè)高階函數(shù),因?yàn)樗邮找粋€(gè)函數(shù)作為實(shí)際參數(shù)。

一個(gè)高階函數(shù)還可以輸出另一個(gè)函數(shù),比如:

function foo() {
    var fn = function inner(msg){
        console.log( msg );
    };

    return fn;
}

var f = foo();

f( "Hello!" );          // Hello!

return 不是“輸出”另一個(gè)函數(shù)的唯一方法:

function foo() {
    var fn = function inner(msg){
        console.log( msg );
    };

    bar( fn );
}

function bar(func) {
    func( "Hello!" );
}

foo();                  // Hello!

高階函數(shù)的定義就是將其他函數(shù)看做值的函數(shù)。FP 程序員一天到晚都在寫(xiě)這些東西!

保持作用域

在一切編程方式 —— 特別是 FP —— 中最強(qiáng)大的東西之一,就是當(dāng)一個(gè)函數(shù)位于另一個(gè)函數(shù)的作用域中時(shí)如何動(dòng)作。當(dāng)內(nèi)部函數(shù)引用外部函數(shù)的一個(gè)變量時(shí),這稱(chēng)為閉包(closure)。

實(shí)用的定義是,閉包是在一個(gè)函數(shù)即使在不同的作用域中被執(zhí)行時(shí),也能記住并訪問(wèn)它自己作用域之外的變量。

考慮如下代碼:

function foo(msg) {
    var fn = function inner(){
        console.log( msg );
    };

    return fn;
}

var helloFn = foo( "Hello!" );

helloFn();              // Hello!

foo(..) 的作用域中的形式參數(shù)變量 msg 在內(nèi)部函數(shù)中被引用了。當(dāng) foo(..) 被執(zhí)行,內(nèi)部函數(shù)被創(chuàng)建時(shí),它就會(huì)捕獲對(duì) msg 變量的訪問(wèn)權(quán),并且即使在被 return 之后依然保持這個(gè)訪問(wèn)權(quán)。

一旦我們有了 helloFn,一個(gè)內(nèi)部函數(shù)的引用,foo(..) 已經(jīng)完成運(yùn)行而且它的作用域看起來(lái)應(yīng)當(dāng)已經(jīng)消失了,這意味著變量 msg 將不復(fù)存在。但是這沒(méi)有發(fā)生,因?yàn)閮?nèi)部函數(shù)擁有一個(gè)對(duì) msg 的閉包使它保持存在。只要這個(gè)內(nèi)部函數(shù)(現(xiàn)在在一個(gè)不同的作用域中通過(guò) helloFn 引用)存在,被閉包的變量 msg 就會(huì)保持下來(lái)。

再讓我們看幾個(gè)閉包在實(shí)際中的例子:

function person(id) {
    var randNumber = Math.random();

    return function identify(){
        console.log( "I am " + id + ": " + randNumber );
    };
}

var fred = person( "Fred" );
var susan = person( "Susan" );

fred();                 // I am Fred: 0.8331252801601532
susan();                // I am Susan: 0.3940753308893741

內(nèi)部函數(shù) identify() 閉包著兩個(gè)變量,形式參數(shù) id 和內(nèi)部變量 randNumber

閉包允許的訪問(wèn)權(quán)不僅僅限于讀取變量的原始值 —— 它不是一個(gè)快照而是一個(gè)實(shí)時(shí)鏈接。你可以更新這個(gè)值,而且在下一次訪問(wèn)之前這個(gè)新的當(dāng)前狀態(tài)會(huì)被一直記住。

function runningCounter(start) {
    var val = start;

    return function current(increment = 1){
        val = val + increment;
        return val;
    };
}

var score = runningCounter( 0 );

score();                // 1
score();                // 2
score( 13 );            // 15

警告: 由于我們將在本文稍后講解的一些理由,這種使用閉包來(lái)記住改變的狀態(tài)(val)的例子可能是你想要盡量避免的。

如果你有一個(gè)操作需要兩個(gè)輸入,你現(xiàn)在知道其中之一但另一個(gè)將會(huì)在稍后指定,你就可以使用閉包來(lái)記住第一個(gè)輸入:

function makeAdder(x) {
    return function sum(y){
        return x + y;
    };
}

// 我們已經(jīng)知道 `10` 和 `37` 都是第一個(gè)輸入了
var addTo10 = makeAdder( 10 );
var addTo37 = makeAdder( 37 );

// 稍后,我們指定第二個(gè)輸入
addTo10( 3 );           // 13
addTo10( 90 );          // 100

addTo37( 13 );          // 50

一般說(shuō)來(lái),一個(gè) sum(..) 函數(shù)將會(huì)拿著 xy 兩個(gè)輸入并把它們加在一起。但是在這個(gè)例子中我們首先收到并(通過(guò)閉包)記住值 x,而值 y 是在稍后被分離地指定的。

注意: 這種在連續(xù)的函數(shù)調(diào)用中指定輸入的技術(shù)在 FP 中非常常見(jiàn),而且擁有兩種形式:局部應(yīng)用(partial application)與柯里化(currying)。我們將在本書(shū)稍后更徹底地深入它們。

當(dāng)然,因?yàn)樵?JS 中函數(shù)只是一種值,所以我們可以通過(guò)閉包來(lái)記住函數(shù)值。

function formatter(formatFn) {
    return function inner(str){
        return formatFn( str );
    };
}

var lower = formatter( function formatting(v){
    return v.toLowerCase();
} );

var upperFirst = formatter( function formatting(v){
    return v[0].toUpperCase() + v.substr( 1 ).toLowerCase();
} );

lower( "WOW" );             // wow
upperFirst( "hello" );      // Hello

與其將 toUpperCase()toLowerCase() 的邏輯在我們的代碼中散布/重復(fù)得到處都是,F(xiàn)P 鼓勵(lì)我們創(chuàng)建封裝(encapsulate) —— “包起來(lái)”的炫酷說(shuō)法 —— 這種行為的簡(jiǎn)單函數(shù)。

具體地說(shuō),我們創(chuàng)建了兩個(gè)簡(jiǎn)單的一元函數(shù) lower(..)upperFirst(..),在我們程序的其余部分中,這些函數(shù)將會(huì)更容易地與其他函數(shù)組合起來(lái)工作。

提示: 你是否發(fā)現(xiàn)了 upperFirst(..) 本可以使用 lower(..)?

我們將在本書(shū)的剩余部分重度依賴(lài)閉包。如果不談?wù)麄€(gè)編程世界,它可能是一切 FP 中最重要的基礎(chǔ)實(shí)踐。要非常熟悉它!

語(yǔ)法

在我們從這個(gè)函數(shù)的入門(mén)教程啟程之前,讓我們花點(diǎn)兒時(shí)間討論一下它們的語(yǔ)法。

與本書(shū)的其他許多部分不同,這一節(jié)中的討論帶有最多的個(gè)人意見(jiàn)與偏好,不論你是否同意或者反對(duì)這里出現(xiàn)的看法。這些想法非常主觀,雖然看起來(lái)許多人感覺(jué)它們更絕對(duì)。不過(guò)說(shuō)到頭來(lái),由你決定。

名稱(chēng)有何含義?

從語(yǔ)法上講,函數(shù)聲明要求包含一個(gè)名稱(chēng):

function helloMyNameIs() {
    // ..
}

但是函數(shù)表達(dá)式可以以命名和匿名兩種形式出現(xiàn):

foo( function namedFunctionExpr(){
    // ..
} );

bar( function(){    // <-- 看,沒(méi)有名稱(chēng)!
    // ..
} );

順便問(wèn)一下,我們說(shuō)匿名究竟是什么意思?具體地講,函數(shù)有一個(gè) name 屬性,它持有這個(gè)函數(shù)在語(yǔ)法上被賦予的名稱(chēng)的字符串值,比如 "helloMyNameIs" 或者 "namedFunctionExpr"。這個(gè) name 屬性最常被用于你的 JS 環(huán)境的控制臺(tái)/開(kāi)發(fā)者工具中,當(dāng)這個(gè)函數(shù)存在于調(diào)用棧中時(shí)將它顯示出來(lái)。

匿名函數(shù)通常被顯示為 (anonymous function)

如果你曾經(jīng)在除了一個(gè)異常的調(diào)用棧軌跡以外沒(méi)有任何可用信息的情況下調(diào)試 JS 程序,你就可能感受過(guò)看到一行接一行的 (anonymous function) 的痛苦。對(duì)于該異常從何而來(lái),這種列表不會(huì)給開(kāi)發(fā)者任何線索。它幫不到開(kāi)發(fā)者。

如果你給你的函數(shù)表達(dá)式命名,那么這個(gè)名稱(chēng)將總是被使用。所以如果你使用了一個(gè)像 handleProfileClicks 這樣的好名字取代 foo,那么你將得到有用得多的調(diào)用棧軌跡。

在 ES6 中,匿名函數(shù)表達(dá)式可以被 名稱(chēng)推斷(name inferencing) 所輔助。考慮如下代碼:

var x = function(){};

x.name;         // x

如果引擎能夠猜測(cè)你 可能 想讓這個(gè)函數(shù)叫什么名字,它就會(huì)立即這么做。

但要小心,不是所有的語(yǔ)法形式都能從名稱(chēng)推斷中受益。函數(shù)表達(dá)式可能最常出現(xiàn)的地方就是作為一個(gè)函數(shù)調(diào)用的實(shí)際參數(shù):

function foo(fn) {
    console.log( fn.name );
}

var x = function(){};

foo( x );               // x
foo( function(){} );    //

當(dāng)從最近的外圍語(yǔ)法中無(wú)法推斷名稱(chēng)時(shí),它會(huì)保留一個(gè)空字符串。這樣的函數(shù)將會(huì)在調(diào)用棧軌跡中報(bào)告為 (anonymous function)

除了調(diào)試的問(wèn)題之外,被命名的函數(shù)還有其他的好處。首先,語(yǔ)法名稱(chēng)(也叫詞法名稱(chēng))對(duì)于內(nèi)部自引用十分有用。自引用對(duì)于遞歸來(lái)說(shuō)是必要的,在事件處理器中也十分有幫助。

考慮這些不同的場(chǎng)景:

// 同步遞歸:
function findPropIn(propName,obj) {
    if (obj == undefined || typeof obj != "object") return;

    if (propName in obj) {
        return obj[propName];
    }
    else {
        let props = Object.keys( obj );
        for (let i = 0; i < props.length; i++) {
            let ret = findPropIn( propName, obj[props[i]] );
            if (ret !== undefined) {
                return ret;
            }
        }
    }
}
// 異步遞歸
setTimeout( function waitForIt(){
    // `it` 還存在嗎?
    if (!o.it) {
        // 稍后重試
        setTimeout( waitForIt, 100 );
    }
}, 100 );
// 解除事件處理器綁定
document.getElementById( "onceBtn" )
    .addEventListener( "click", function handleClick(evt){
        // 解除事件綁定
        evt.target.removeEventListener( "click", handleClick, false );

        // ..
    }, false );

在所有這些情況下,命名函數(shù)的名稱(chēng)都是它內(nèi)部的一個(gè)有用且可靠的自引用。

另外,即使是在一個(gè)一行函數(shù)的簡(jiǎn)單情況下,將它們命名也會(huì)使代碼更具自解釋性,因此使代碼對(duì)于那些以前沒(méi)有讀過(guò)它的人來(lái)說(shuō)變得更易讀:

people.map( function getPreferredName(person){
    return person.nicknames[0] || person.firstName;
} )
// ..

函數(shù)名 getPreferredName(..) 告訴讀者映射操作的意圖是什么,而這僅從代碼來(lái)看的話沒(méi)那么明顯。這個(gè)名稱(chēng)標(biāo)簽使得代碼更具可讀性。

另一個(gè)匿名函數(shù)表達(dá)式常見(jiàn)的地方是 IIFE(即時(shí)調(diào)用的函數(shù)表達(dá)式):

(function(){

    // 看,我是一個(gè) IIFE!

})();

你幾乎永遠(yuǎn)看不到 IIFE 為它們的函數(shù)表達(dá)式使用名稱(chēng),但它們應(yīng)該這么做。為什么?為了我們剛剛講過(guò)的所有理由:調(diào)用棧軌跡調(diào)試、可靠的自引用、與可讀性。如果你實(shí)在想不出任何其他名稱(chēng),至少要使用 IIFE 這個(gè)詞:

(function IIFE(){

    // 你已經(jīng)知道我是一個(gè) IIFE 了!

})();

我的意思是有多種理由可以解釋為什么 命名函數(shù)總是優(yōu)于匿名函數(shù)。 事實(shí)上,我甚至可以說(shuō)基本上不存在匿名函數(shù)更優(yōu)越的情況。對(duì)于命名的另一半來(lái)說(shuō)它們根本沒(méi)有任何優(yōu)勢(shì)。

編寫(xiě)匿名函數(shù)不可思議地容易,因?yàn)槟菢訒?huì)讓我們投入精力找出的名稱(chēng)減少一個(gè)。

我承認(rèn);我和所有人一樣有罪。我不喜歡在命名上掙扎。我想到的頭三個(gè)或四個(gè)名稱(chēng)通常都很差勁。我不得不一次又一次地重新考慮命名。我寧愿撒手不管而使用匿名函數(shù)表達(dá)式。

但我們是在用好寫(xiě)與難讀做交易。這不是一樁好買(mǎi)賣(mài)。由于懶惰或沒(méi)有創(chuàng)意而不想為你的函數(shù)找出名稱(chēng),是一個(gè)使用匿名函數(shù)的太常見(jiàn),但很爛的借口。

為每個(gè)函數(shù)命名。 如果你坐在那里很為難,不能為你寫(xiě)的某個(gè)函數(shù)想出一個(gè)好名字,那么我會(huì)強(qiáng)烈地感覺(jué)到你還沒(méi)有完全理解這個(gè)函數(shù)的目的 —— 或者它的目的太泛泛或太抽象了。你需要回過(guò)頭去重新設(shè)計(jì)這個(gè)函數(shù),直到它變得更清晰。而到了那個(gè)時(shí)候,一個(gè)名稱(chēng)將顯而易見(jiàn)。

我可以用我的經(jīng)驗(yàn)作證,在給某個(gè)東西良好命名的掙扎中,我通常對(duì)它有了更好的理解,甚至經(jīng)常為了改進(jìn)可讀性和可維護(hù)性而重構(gòu)它的設(shè)計(jì)。這種時(shí)間上的投資是值得的。

沒(méi)有 function 的函數(shù)

至此我們一直在使用完全規(guī)范的函數(shù)語(yǔ)法。但毫無(wú)疑問(wèn)你也聽(tīng)說(shuō)過(guò)關(guān)于新的 ES6 => 箭頭函數(shù)語(yǔ)法的討論。

比較一下:

people.map( function getPreferredName(person){
    return person.nicknames[0] || person.firstName;
} )
// ..

people.map( person => person.nicknames[0] || person.firstName );

哇哦。

關(guān)鍵詞 function 不見(jiàn)了,return、括號(hào) ( )、花括號(hào) { }、和引號(hào) ; 也不見(jiàn)了。所有這些,換來(lái)了所謂的大箭頭符號(hào) =>

但這里我們忽略了另一個(gè)東西。你發(fā)現(xiàn)了嗎?函數(shù)名 getPreferredName

沒(méi)錯(cuò);=> 箭頭函數(shù)是詞法上匿名的;沒(méi)有辦法在語(yǔ)法上給它提供一個(gè)名稱(chēng)。它們的名稱(chēng)可以像普通函數(shù)那樣被推斷,但同樣地,在最常見(jiàn)的函數(shù)表達(dá)式作為實(shí)際參數(shù)的情況下它幫不上什么忙。

如果由于某些原因 person.nicknames 沒(méi)有被定義,一個(gè)異常被拋出,這意味著 (anonymous function) 將會(huì)位于調(diào)用棧軌跡的頂端。呃。

老實(shí)說(shuō),對(duì)我而言,=> 箭頭函數(shù)的匿名性是一把指向心臟的 => 匕首。我無(wú)法忍受命名的缺失。它更難讀、更難調(diào)試、而且不可能進(jìn)行自引用。

如果說(shuō)這還不夠壞,那另一個(gè)打臉的地方是,如果你的函數(shù)定義有不同的場(chǎng)景,你就必須趟過(guò)一大堆有微妙不同的語(yǔ)法。我不會(huì)在這里涵蓋它們的所有細(xì)節(jié),但簡(jiǎn)單地說(shuō):

people.map( person => person.nicknames[0] || person.firstName );

// 多個(gè)形式參數(shù)?需要 ( )
people.map( (person,idx) => person.nicknames[0] || person.firstName );

// 形式參數(shù)解構(gòu)?需要 ( )
people.map( ({ person }) => person.nicknames[0] || person.firstName );

// 形式參數(shù)默認(rèn)值?需要 ( )
people.map( (person = {}) => person.nicknames[0] || person.firstName );

// 返回一個(gè)對(duì)象?需要 ( )
people.map( person =>
    ({ preferredName: person.nicknames[0] || person.firstName })
);

在 FP 世界中 => 激動(dòng)人心的地方主要在于它幾乎完全符合數(shù)學(xué)上函數(shù)的符號(hào),特別是在像 Haskell 這樣的 FP 語(yǔ)言中。箭頭函數(shù)語(yǔ)法 => 的形狀可以進(jìn)行數(shù)學(xué)上的交流。

再挖深一些,我覺(jué)得支持 => 的爭(zhēng)辯是,通過(guò)使用輕量得多的語(yǔ)法,我們減少了函數(shù)之間的視覺(jué)邊界,這允許我們像曾經(jīng)使用懶惰表達(dá)式那樣使用簡(jiǎn)單的函數(shù)表達(dá)式 —— 這是另一件 FP 程序員們最喜歡的事。

我想大多數(shù) FP 程序員將會(huì)對(duì)這些問(wèn)題不屑一顧。他們深?lèi)?ài)著匿名函數(shù),也愛(ài)簡(jiǎn)潔的語(yǔ)法。但正如我之前說(shuō)的:這由你來(lái)決定。

注意: 雖然在實(shí)際中我不喜歡在我的應(yīng)用程序中使用 =>,但我們將會(huì)在本書(shū)剩余部分的許多地方使用它 —— 特別是當(dāng)我們展示常用的 FP 工具時(shí) —— 當(dāng)簡(jiǎn)潔性在代碼段有限的物理空間中成為不錯(cuò)的優(yōu)化方式時(shí)。這種方式是否會(huì)使你的代碼可讀性提高或降低,你要做出自己的決斷。

This 是什么?

如果你對(duì) JavaScript 中的 this 綁定規(guī)則不熟悉,我推薦你看看我的“你不懂 JS:this 與對(duì)象原型”一書(shū)。對(duì)于這一節(jié)的目的來(lái)說(shuō),我假定你知道在一個(gè)函數(shù)調(diào)用中 this 是如何被決定的(四種規(guī)則之一)。但就算你對(duì) this 還不甚了解,好消息是我們會(huì)得出這樣的結(jié)論:如果你想使用 FP,那么你就不應(yīng)當(dāng)使用 this

JavaScript 的 function 擁有一個(gè)在每次函數(shù)調(diào)用時(shí)自動(dòng)綁定的 this 關(guān)鍵字。這個(gè) this 關(guān)鍵字可以用許多不同的方式描述,但我喜歡稱(chēng)它為函數(shù)運(yùn)行的對(duì)象上下文環(huán)境。

對(duì)于你的函數(shù)來(lái)說(shuō),this 是一個(gè)隱含形式參數(shù)輸入。

考慮如下代碼:

function sum() {
    return this.x + this.y;
}

var context = {
    x: 1,
    y: 2
};

sum.call( context );        // 3

context.sum = sum;
context.sum();              // 3

var s = sum.bind( context );
s();                        // 3

當(dāng)然,如果 this 可以是一個(gè)函數(shù)的隱含輸入,那么相同的對(duì)象環(huán)境就可以作為明確的實(shí)際參數(shù)發(fā)送:

function sum(ctx) {
    return ctx.x + ctx.y;
}

var context = {
    x: 1,
    y: 2
};

sum( context );

更簡(jiǎn)單。而且這種代碼在 FP 中處理起來(lái)容易得多。當(dāng)輸入總是明確的時(shí)候,將多個(gè)函數(shù)組合在一起,或者使用我們將在下一章中學(xué)到的其他搬弄輸入的技術(shù)都將簡(jiǎn)單得多。要使這些技術(shù)與 this 這樣的隱含輸入一起工作,在不同場(chǎng)景下要么很尷尬要么就是幾乎不可能。

我們可以在一個(gè)基于 this 的系統(tǒng)中利用其他技巧,例如原型委托(也在“this 與對(duì)象原型”一書(shū)中有詳細(xì)講解):

var Auth = {
    authorize() {
        var credentials = this.username + ":" + this.password;
        this.send( credentials, resp => {
            if (resp.error) this.displayError( resp.error );
            else this.displaySuccess();
        } );
    },
    send(/* .. */) {
        // ..
    }
};

var Login = Object.assign( Object.create( Auth ), {
    doLogin(user,pw) {
        this.username = user;
        this.password = pw;
        this.authorize();
    },
    displayError(err) {
        // ..
    },
    displaySuccess() {
        // ..
    }
} );

Login.doLogin( "fred", "123456" );

注意: Object.assign(..) 是一個(gè) ES6+ 工具,用于從一個(gè)或多個(gè)源對(duì)象向一個(gè)目標(biāo)對(duì)象進(jìn)行屬性的淺賦值拷貝:Object.assign( target, source1, ... )

如果你解讀這段代碼有困難:我們有兩個(gè)分離的對(duì)象 LoginAuthLogin 實(shí)施了向 Auth 的原型委托。通過(guò)委托與隱含的 this 上下文環(huán)境共享,這兩個(gè)對(duì)象在 this.authorize() 函數(shù)調(diào)用中被虛擬地組合在一起,這樣在 Auth.authorize(..) 函數(shù)中 this 上的屬性/方法被動(dòng)態(tài)地共享。

由于各種原因這段代碼不符合 FP 的種種原則,但是最明顯的問(wèn)題就是隱含的 this 共享。我們可以使它更明確一些,保持代碼可以更容易地向 FP 的方向靠攏:

// ..

authorize(ctx) {
    var credentials = ctx.username + ":" + ctx.password;
    Auth.send( credentials, function onResp(resp){
        if (resp.error) ctx.displayError( resp.error );
        else ctx.displaySuccess();
    } );
}

// ..

doLogin(user,pw) {
    Auth.authorize( {
        username: user,
        password: pw
    } );
}

// ..

從我的觀點(diǎn)看,這其中的問(wèn)題并不是使用了對(duì)象來(lái)組織行為。而是我們?cè)噲D使用隱含輸入取代明確輸入。當(dāng)我?guī)衔业?FP 帽子時(shí),我會(huì)想將 this 這東西留在衣架上。

總結(jié)

函數(shù)十分強(qiáng)大。

但我們要清楚什么是函數(shù)。它不只是一個(gè)語(yǔ)句/操作的集合。特別地,一個(gè)函數(shù)需要一個(gè)或多個(gè)輸入(理想情況,只有一個(gè)!)以及一個(gè)輸出。

函數(shù)內(nèi)部的函數(shù)可以擁有外部變量的閉包,為稍后的訪問(wèn)記住它們。這是所有種類(lèi)的編程中最重要的概念之一,而且是 FP 基礎(chǔ)的基礎(chǔ)。

要小心匿名函數(shù),特別是箭頭函數(shù) =>。它們寫(xiě)起來(lái)方便,但是將作者的成本轉(zhuǎn)嫁到了讀者身上。我們學(xué)習(xí) FP 的所有原因就是寫(xiě)出可讀性更強(qiáng)的代碼,所以先不要那么快就趕這個(gè)潮流。

不要使用 this 敏感的函數(shù)。別這么干。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,034評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,327評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,554評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,337評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,883評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,114評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,625評(píng)論 1 332
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,555評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,737評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,973評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,362評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,615評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,343評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,699評(píng)論 2 370

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