感謝社區(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 );
a
和 a * 2
(實(shí)際上,是這個(gè)表達(dá)式的值,6
) 是 foo(..)
調(diào)用的 實(shí)際參數(shù)。x
和 y
是接收實(shí)際參數(shù)值(分別是 3
和 6
)的 形式參數(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ù)的引用。你可檢查 arguments
的 length
屬性來(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ù) x
、y
、和 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ì)包括 1
、2
、和 3
這三個(gè)值。...args
聚集所有其余的東西,不包含 x
、y
、和 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ù)變量 x
和 y
,被傳入的對(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)的答案是 2
、2
、8
、和 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ì)拿著 x
和 y
兩個(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ì)象 Login
和 Auth
,Login
實(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ù)。別這么干。