感謝社區(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 );
a
和 a * 2
(實際上况凉,是這個表達(dá)式的值谚鄙,6
) 是 foo(..)
調(diào)用的 實際參數(shù)。x
和 y
是接收實際參數(shù)值(分別是 3
和 6
)的 形式參數(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ù)的引用高镐。你可檢查 arguments
的 length
屬性來搞清楚有多少參數(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ù) x
、y
砖茸、和 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
聚集所有其余的東西江咳,不包含 x
、y
哥放、和 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ù)變量 x
和 y
,被傳入的對象中具有相應(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)為有辦法使輸出的路徑更加明顯鸵隧。
注意: 突擊測驗的答案是 2
、2
意推、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ù)將會拿著 x
和 y
兩個輸入并把它們加在一起。但是在這個例子中我們首先收到并(通過閉包)記住值 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, ... )
汞扎。
如果你解讀這段代碼有困難:我們有兩個分離的對象 Login
和 Auth
,Login
實施了向 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ù)。別這么干时迫。