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

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

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

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

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

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

什么是函數(shù)逆皮?

要解釋函數(shù)式編程宅粥,我所能想到的最自然的起點就是 函數(shù)。這看起來再明顯不過了电谣,但我想我們的旅程需要堅實的第一步秽梅。

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

數(shù)學(xué)簡憶

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

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

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

你能注意到,對于任何 x 的值攒读,比如 2朵诫,如果你將它插入這個等式,你會得到 11薄扁。那么 11 是什么剪返?它是函數(shù) f(x)返回值废累,代表我們剛才說到的 y 值。

換句話說随夸,在圖像的曲線上有一個點 (2,11)九默。而且對于我們插入的任意的 x 的值,我們都能得到另一個與之相對應(yīng)的 y 值作為一個點的坐標(biāo)宾毒。比如另外一個點 (0,3)驼修,以及另一個點 (-1,5)。將這些點放在一起诈铛,你就得到了上面的拋物線圖像乙各。

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

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

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

函數(shù) vs 過程

那么為什么說了半天數(shù)學(xué)和圖像?因為在某種意義上撒轮,函數(shù)式編程就是以這種數(shù)學(xué)意義上的 函數(shù) 來使用函數(shù)乞旦。

你可能更習(xí)慣于將函數(shù)考慮為過程(procedures)。它有什么區(qū)別题山?一個任意功能的集合兰粉。它可能有輸入,也可能沒有臀蛛。它可能有一個輸出(return 值)亲桦,也可能沒有。

而一個函數(shù)接收輸入并且絕對總是有一個 return 值浊仆。

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

函數(shù)輸入

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

你有時會聽到人們稱它們?yōu)椤皩嶋H參數(shù)(arguments)”郊尝,而有時稱為“形式參數(shù)(parameters)”二跋。那么這都是什么意思?

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

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

var a = 3;

foo( a, a * 2 );

aa * 2(實際上况凉,是這個表達(dá)式的值谚鄙,6) 是 foo(..) 調(diào)用的 實際參數(shù)xy 是接收實際參數(shù)值(分別是 36)的 形式參數(shù)刁绒。

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

輸入計數(shù)

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

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

foo(..) 期待 三個實際參數(shù)腥光,因為它擁有三個被聲明的形式參數(shù)。這個數(shù)量有一個特殊的術(shù)語:元(arity)糊秆。元是函數(shù)聲明中形式參數(shù)的數(shù)量武福。foo(..) 的元是 3

你可能會想在運行時期間檢查一個函數(shù)引用來判定它的元痘番。這可以通過這個函數(shù)引用的 length 屬性來完成:

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

foo.length;             // 3

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

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

// `fn` 被設(shè)置為某個函數(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 );
}

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

一個要小心的坑是母市,特定種類的形式參數(shù)列表可以使函數(shù)的 length 屬性報告的東西與你期待的不同矾兜。不要擔(dān)心,我們會在本章稍后講解每一種(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 值可能會使你驚訝椅寺。

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

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

foo( 3, 4 );

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

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

小心:絕不要 按位置訪問實際參數(shù)胚股,比如 arguments[1]。如果你必須這么做的話裙秋,堅持只使用 arguments.length琅拌。

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

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

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

好了余爆,在這一點上嘮叨得夠多了。

假定你需要以一種類似數(shù)組下標(biāo)定位的方式來訪問實際參數(shù)夸盟,這可能是因為你正在訪問一個沒有正式形式參數(shù)位置的實際參數(shù)蛾方。我們該如何做?

ES6 前來拯救上陕!讓我們使用 ... 操作符來聲明我們的函數(shù) —— 它有多個名稱:“擴散”桩砰、“剩余”、或者(我最喜歡的)“聚集”释簿。

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

看到形式參數(shù)列表中的 ...args 了嗎亚隅?這是一種新的 ES6 聲明形式,它告訴引擎去收集(嗯哼庶溶,聚集)所有剩余的(如果有的話)沒被賦值給命名形式參數(shù)的實際參數(shù)煮纵,并將它們放到名為 args 的真正的數(shù)組中。args 將總是一個數(shù)組偏螺,即便是空的醉途。但它 不會 包含那些已經(jīng)賦值給形式參數(shù) xy砖茸、和 z 的值,只有超過前三個值被傳入的所有東西殴穴。

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è)計一個解析任意多實際參數(shù)的函數(shù),就在末尾使用 ...args(或你喜歡的其他任何名字)〔苫希現(xiàn)在劲够,你將得到一個真正的,沒有被廢棄的休傍,不討人嫌的數(shù)組來訪問那些實際參數(shù)征绎。

只不過要注意,值 4 在這個 args 的位置 0 上,而不是位置 3人柿。而且它的 length 值將不會包括 1柴墩、2、和 3 這三個值凫岖。...args 聚集所有其余的東西江咳,不包含 xy哥放、和 z歼指。

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

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

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

說到 ES6 的好處呵哨,關(guān)于你函數(shù)的實際參數(shù)與形式參數(shù)赁濒,還有幾種你可能想知道的其他的技巧。這個簡要概覽之外的更多信息孟害,參見我的 “你不懂 JS —— ES6 與未來” 的第二章拒炎。

實際參數(shù)技巧

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

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

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

foo( ...arr );                      // 4

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

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

var arr = [ 2 ];

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

以這種對稱的感覺考慮 ...:在一個值的列表的位置,它 擴散揪荣。在一個賦值的位置 —— 比如形式參數(shù)列表筷黔,因為實際參數(shù)被 賦值給 了形式參數(shù) —— 它 聚集

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

形式參數(shù)技巧

在 ES6 中请祖, 形式參數(shù)可以被聲明 默認(rèn)值订歪。在這個形式參數(shù)的實際參數(shù)沒有被傳遞,或者被傳遞了一個 undefined 值的情況下损拢,默認(rèn)的賦值表達(dá)式將會取而代之陌粹。

考慮如下代碼:

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

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

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

另一個我們可以在形式參數(shù)列表中使用的技巧稱為 “解構(gòu)”抒和。我們將簡要地掃它一眼,因為這個話題要比我們在這里討論的復(fù)雜太多了彤蔽。同樣摧莽,更多信息參考我的 “ES6 與未來”。

還記得剛才可以接收318個實際參數(shù)的 foo(..) 嗎6倩尽镊辕?

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

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

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

function foo(args) {
    // ..
}

foo( [1,2,3] );

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

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

foo( [1,2,3] );

你發(fā)現(xiàn)現(xiàn)在形式參數(shù)列表周圍的方括號 [ .. ] 了嗎?這就是數(shù)組解構(gòu)删性。解構(gòu)為你想看到的某種結(jié)構(gòu)(對象亏娜,數(shù)組等)聲明了一個 范例,描述應(yīng)當(dāng)如何將它分解(分配)為各個獨立的部分镇匀。

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

你本可以像下面這樣手動地做同樣的事情:

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

    // ..
}

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

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

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

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

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

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

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

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

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

我們不必在這個調(diào)用點中說明 x牛曹,因為我們實際上不關(guān)心 x佛点。我們只是忽略它,而不是必須去做傳入 undefined 作為占位符這樣令人分心的事情黎比。

有些語言直接擁有這種行為特性:命名實際參數(shù)超营。換句話說,在調(diào)用點中阅虫,給一個輸入值打上一個標(biāo)簽來指示它映射到哪個形式參數(shù)上演闭。JavaScript 不具備命名實際參數(shù),但是形式參數(shù)對象解構(gòu)是最佳后備選項颓帝。

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

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

注意: 在第三章中锣枝,我們將重溫這種命名實際參數(shù)解構(gòu)技巧厢拭,來對付惱人的形式參數(shù)順序問題。

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

考慮這個函數(shù):

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

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

例如:

foo( 3, 4 );            // 12

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

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

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

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

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

警告: 要對這里的 方便 的沖動特別小心塑娇。僅僅因為你可以這樣設(shè)計一個函數(shù),而且即便可能立即感知到一些好處劫侧,這種設(shè)計決定所帶來的長期成本也可能不令人愉快埋酬。

函數(shù)輸出

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

function foo() {}

function bar() {
    return;
}

function baz() {
    return undefined;
}

如果你沒有 return 或者你僅僅有一個空的 return;写妥,那么 undefined 值就會被隱含地 return

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

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

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

就像解構(gòu)允許我們在形式參數(shù)中拆分?jǐn)?shù)組/對象一樣呼猪,我們也可以在普通的賦值中這么做:

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

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

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

提示: 如果我沒有這么提醒你轴踱,那將是我的疏忽:花點時間考慮一下症脂,一個需要多個輸出的函數(shù)是否能夠被重構(gòu)來避免這種情況,也許分成兩個或更多更小的意圖單一的函數(shù)淫僻?有時候這是可能的诱篷,有時候不;但你至少應(yīng)該考慮一下雳灵。

提前返回

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

考慮如下代碼:

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

突擊測驗:不使用瀏覽器運行這段代碼针贬,foo(2) 返回什么?foo(4) 呢拢蛋?foo(8) 呢桦他?foo(12) 呢?

你對自己的答案有多自信谆棱?你為這些答案交了多少智商稅快压?我考慮它時,前兩次都錯了垃瞧,而且我是用寫的蔫劣!

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

注意: 突擊測驗的答案是 22意推、8豆瘫、和 13.

考慮一下這個版本的代碼:

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

這個版本無疑更加繁冗。但我要爭辯的是它的邏輯追溯起來更簡單菊值,因為每一個 retValue 可能被設(shè)置的分支都被一個檢查它是否已經(jīng)被設(shè)置過的條件 守護 著外驱。

我們沒有提前從函數(shù)中 return 出來育灸,而是使用了普通的流程控制來決定 retValue 的賦值。最后昵宇,我們單純地 return retValue磅崭。

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

沒有被 return 的輸出

你可能在你寫過的大部分代碼中用過割岛,但可能沒有太多考慮過的技術(shù)之一,就是通過簡單地改變函數(shù)外部的變量來使它輸出一些或全部的值犯助。

記得我們在本章早先的 <code>f(x) = 2x2 + 3</code> 函數(shù)嗎癣漆?我們可以用 JS 這樣定義它:

var y;

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

foo( 2 );

y;                      // 11

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

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

var y = foo( 2 );

y;                      // 11

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

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

但是改變外部作用域中的變量把跨,就像我們在 foo(..) 內(nèi)部中對 y 賦值所做的人弓,只是得到隱含輸出的方式之一。一個更微妙的例子是通過引用來改變非本地值着逐。

考慮如下代碼:

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

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

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

為什么省有?因為 list 持有一個 nums 引用的引用拷貝痒留,而不是數(shù)組值 [1,3,9,..] 的值拷貝谴麦。因為 JS 對數(shù)組,對象伸头,以及函數(shù)使用引用和引用拷貝匾效,所以我們可以很容易地從我們的函數(shù)中制造輸出,這甚至是偶然的恤磷。

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

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

函數(shù)可以接收并返回任意類型的值锌妻。一個接收或返回一個或多個其他函數(shù)的函數(shù)有一個特殊的名稱:高階函數(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(..) 是一個高階函數(shù)旬牲,因為它接收一個函數(shù)作為實際參數(shù)仿粹。

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

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

    return fn;
}

var f = foo();

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

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

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

    bar( fn );
}

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

foo();                  // Hello!

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

保持作用域

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

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

考慮如下代碼:

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)建時,它就會捕獲對 msg 變量的訪問權(quán)茎用,并且即使在被 return 之后依然保持這個訪問權(quán)遣总。

一旦我們有了 helloFn,一個內(nèi)部函數(shù)的引用轨功,foo(..) 已經(jīng)完成運行而且它的作用域看起來應(yīng)當(dāng)已經(jīng)消失了旭斥,這意味著變量 msg 將不復(fù)存在。但是這沒有發(fā)生古涧,因為內(nèi)部函數(shù)擁有一個對 msg 的閉包使它保持存在琉预。只要這個內(nèi)部函數(shù)(現(xiàn)在在一個不同的作用域中通過 helloFn 引用)存在,被閉包的變量 msg 就會保持下來蒿褂。

再讓我們看幾個閉包在實際中的例子:

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() 閉包著兩個變量圆米,形式參數(shù) id 和內(nèi)部變量 randNumber卒暂。

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

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

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

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

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

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

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

addTo37( 13 );          // 50

一般說來奖亚,一個 sum(..) 函數(shù)將會拿著 xy 兩個輸入并把它們加在一起。但是在這個例子中我們首先收到并(通過閉包)記住值 x析砸,而值 y 是在稍后被分離地指定的昔字。

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

當(dāng)然,因為在 JS 中函數(shù)只是一種值弦疮,所以我們可以通過閉包來記住函數(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 鼓勵我們創(chuàng)建封裝(encapsulate) —— “包起來”的炫酷說法 —— 這種行為的簡單函數(shù)胁塞。

具體地說咏尝,我們創(chuàng)建了兩個簡單的一元函數(shù) lower(..)upperFirst(..),在我們程序的其余部分中啸罢,這些函數(shù)將會更容易地與其他函數(shù)組合起來工作状土。

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

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

語法

在我們從這個函數(shù)的入門教程啟程之前训桶,讓我們花點兒時間討論一下它們的語法累驮。

與本書的其他許多部分不同,這一節(jié)中的討論帶有最多的個人意見與偏好舵揭,不論你是否同意或者反對這里出現(xiàn)的看法谤专。這些想法非常主觀,雖然看起來許多人感覺它們更絕對午绳。不過說到頭來置侍,由你決定。

名稱有何含義?

從語法上講蜡坊,函數(shù)聲明要求包含一個名稱:

function helloMyNameIs() {
    // ..
}

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

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

bar( function(){    // <-- 看杠输,沒有名稱!
    // ..
} );

順便問一下秕衙,我們說匿名究竟是什么意思蠢甲?具體地講,函數(shù)有一個 name 屬性据忘,它持有這個函數(shù)在語法上被賦予的名稱的字符串值鹦牛,比如 "helloMyNameIs" 或者 "namedFunctionExpr"。這個 name 屬性最常被用于你的 JS 環(huán)境的控制臺/開發(fā)者工具中勇吊,當(dāng)這個函數(shù)存在于調(diào)用棧中時將它顯示出來曼追。

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

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

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

在 ES6 中射亏,匿名函數(shù)表達(dá)式可以被 名稱推斷(name inferencing) 所輔助〗咭担考慮如下代碼:

var x = function(){};

x.name;         // x

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

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

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

var x = function(){};

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

當(dāng)從最近的外圍語法中無法推斷名稱時,它會保留一個空字符串咐柜。這樣的函數(shù)將會在調(diào)用棧軌跡中報告為 (anonymous function)兼蜈。

除了調(diào)試的問題之外,被命名的函數(shù)還有其他的好處拙友。首先为狸,語法名稱(也叫詞法名稱)對于內(nèi)部自引用十分有用。自引用對于遞歸來說是必要的遗契,在事件處理器中也十分有幫助辐棒。

考慮這些不同的場景:

// 同步遞歸:
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ù)的名稱都是它內(nèi)部的一個有用且可靠的自引用漾根。

另外泰涂,即使是在一個一行函數(shù)的簡單情況下,將它們命名也會使代碼更具自解釋性立叛,因此使代碼對于那些以前沒有讀過它的人來說變得更易讀:

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

函數(shù)名 getPreferredName(..) 告訴讀者映射操作的意圖是什么负敏,而這僅從代碼來看的話沒那么明顯。這個名稱標(biāo)簽使得代碼更具可讀性秘蛇。

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

(function(){

    // 看其做,我是一個 IIFE!

})();

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

(function IIFE(){

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

})();

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

編寫匿名函數(shù)不可思議地容易青扔,因為那樣會讓我們投入精力找出的名稱減少一個源织。

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

但我們是在用好寫與難讀做交易。這不是一樁好買賣犁珠。由于懶惰或沒有創(chuàng)意而不想為你的函數(shù)找出名稱傅瞻,是一個使用匿名函數(shù)的太常見,但很爛的借口盲憎。

為每個函數(shù)命名嗅骄。 如果你坐在那里很為難,不能為你寫的某個函數(shù)想出一個好名字饼疙,那么我會強烈地感覺到你還沒有完全理解這個函數(shù)的目的 —— 或者它的目的太泛泛或太抽象了溺森。你需要回過頭去重新設(shè)計這個函數(shù)慕爬,直到它變得更清晰。而到了那個時候屏积,一個名稱將顯而易見医窿。

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

沒有 function 的函數(shù)

至此我們一直在使用完全規(guī)范的函數(shù)語法独榴。但毫無疑問你也聽說過關(guān)于新的 ES6 => 箭頭函數(shù)語法的討論。

比較一下:

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

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

哇哦奕枝。

關(guān)鍵詞 function 不見了棺榔,return、括號 ( )隘道、花括號 { }症歇、和引號 ; 也不見了。所有這些谭梗,換來了所謂的大箭頭符號 =>忘晤。

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

沒錯;=> 箭頭函數(shù)是詞法上匿名的缩幸;沒有辦法在語法上給它提供一個名稱壹置。它們的名稱可以像普通函數(shù)那樣被推斷竞思,但同樣地表谊,在最常見的函數(shù)表達(dá)式作為實際參數(shù)的情況下它幫不上什么忙。

如果由于某些原因 person.nicknames 沒有被定義盖喷,一個異常被拋出爆办,這意味著 (anonymous function) 將會位于調(diào)用棧軌跡的頂端。呃课梳。

老實說距辆,對我而言,=> 箭頭函數(shù)的匿名性是一把指向心臟的 => 匕首暮刃。我無法忍受命名的缺失跨算。它更難讀、更難調(diào)試椭懊、而且不可能進(jìn)行自引用诸蚕。

如果說這還不夠壞,那另一個打臉的地方是,如果你的函數(shù)定義有不同的場景背犯,你就必須趟過一大堆有微妙不同的語法坏瘩。我不會在這里涵蓋它們的所有細(xì)節(jié),但簡單地說:

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

// 多個形式參數(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 );

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

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

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

我想大多數(shù) FP 程序員將會對這些問題不屑一顧升筏。他們深愛著匿名函數(shù)撑柔,也愛簡潔的語法。但正如我之前說的:這由你來決定您访。

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

This 是什么?

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

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

對于你的函數(shù)來說脐恩,this 是一個隱含形式參數(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 可以是一個函數(shù)的隱含輸入驶冒,那么相同的對象環(huán)境就可以作為明確的實際參數(shù)發(fā)送:

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

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

sum( context );

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

我們可以在一個基于 this 的系統(tǒng)中利用其他技巧,例如原型委托(也在“this 與對象原型”一書中有詳細(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(..) 是一個 ES6+ 工具贴谎,用于從一個或多個源對象向一個目標(biāo)對象進(jìn)行屬性的淺賦值拷貝:Object.assign( target, source1, ... )汞扎。

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

由于各種原因這段代碼不符合 FP 的種種原則痹扇,但是最明顯的問題就是隱含的 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
    } );
}

// ..

從我的觀點看鲫构,這其中的問題并不是使用了對象來組織行為。而是我們試圖使用隱含輸入取代明確輸入玫坛。當(dāng)我?guī)衔业?FP 帽子時结笨,我會想將 this 這東西留在衣架上。

總結(jié)

函數(shù)十分強大湿镀。

但我們要清楚什么是函數(shù)炕吸。它不只是一個語句/操作的集合。特別地勉痴,一個函數(shù)需要一個或多個輸入(理想情況赫模,只有一個!)以及一個輸出蚀腿。

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

要小心匿名函數(shù)筛谚,特別是箭頭函數(shù) =>磁玉。它們寫起來方便,但是將作者的成本轉(zhuǎn)嫁到了讀者身上驾讲。我們學(xué)習(xí) FP 的所有原因就是寫出可讀性更強的代碼蚊伞,所以先不要那么快就趕這個潮流席赂。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颅停,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子掠拳,更是在濱河造成了極大的恐慌癞揉,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件溺欧,死亡現(xiàn)場離奇詭異喊熟,居然都是意外死亡,警方通過查閱死者的電腦和手機姐刁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門芥牌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人聂使,你說我怎么就攤上這事壁拉。” “怎么了柏靶?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵扇商,是天一觀的道長。 經(jīng)常有香客問我宿礁,道長案铺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任梆靖,我火速辦了婚禮控汉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘返吻。我一直安慰自己姑子,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布测僵。 她就那樣靜靜地躺著街佑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪捍靠。 梳的紋絲不亂的頭發(fā)上沐旨,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音榨婆,去河邊找鬼磁携。 笑死,一個胖子當(dāng)著我的面吹牛良风,可吹牛的內(nèi)容都是我干的谊迄。 我是一名探鬼主播闷供,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼统诺!你這毒婦竟也來了歪脏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤粮呢,失蹤者是張志新(化名)和其女友劉穎唾糯,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鬼贱,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡移怯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了这难。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舟误。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖姻乓,靈堂內(nèi)的尸體忽然破棺而出嵌溢,到底是詐尸還是另有隱情,我是刑警寧澤蹋岩,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布赖草,位于F島的核電站,受9級特大地震影響剪个,放射性物質(zhì)發(fā)生泄漏秧骑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一扣囊、第九天 我趴在偏房一處隱蔽的房頂上張望乎折。 院中可真熱鬧,春花似錦侵歇、人聲如沸骂澄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坟冲。三九已至,卻和暖如春溃蔫,著一層夾襖步出監(jiān)牢的瞬間健提,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工酒唉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留矩桂,地道東北人沸移。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓痪伦,卻偏偏與公主長得像侄榴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子网沾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

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