翻譯連載 |《JavaScript 輕量級(jí)函數(shù)式編程》- 第 2 章:函數(shù)基礎(chǔ)

第 2 章:函數(shù)基礎(chǔ)

函數(shù)式編程不是僅僅用 function 這個(gè)關(guān)鍵詞來(lái)編程目养。如果真這么簡(jiǎn)單挂滓,那我這本書(shū)可以到此為止了馒索!重點(diǎn)在于:函數(shù)函數(shù)式編程的核心岖食。這也是如何使用函數(shù)(function)才能使我們的代碼具有函數(shù)式(functional)的方法红碑。

然而,你真的明白函數(shù)的含義嗎县耽?

在這一章句喷,我們將會(huì)介紹函數(shù)的基礎(chǔ)知識(shí)镣典,為閱讀本書(shū)的后續(xù)章節(jié)打下基礎(chǔ)兔毙。從某些方面來(lái)講,這章回顧的函數(shù)知識(shí)并不是針對(duì)函數(shù)式編程者兄春,非函數(shù)式編程者同樣需要了解澎剥。但如果我們想要充分、全面地學(xué)習(xí)函數(shù)式編程的概念赶舆,我們需要從里到外地理解函數(shù)哑姚。

請(qǐng)做好準(zhǔn)備,因?yàn)檫€有好多你未知的函數(shù)知識(shí)芜茵。

什么是函數(shù)叙量?

針對(duì)函數(shù)式編程,很自然而然的我會(huì)想到從函數(shù)開(kāi)始九串。這太明顯不過(guò)了绞佩,但是我認(rèn)為我們需要扎實(shí)地走好旅程的第一步。

所以......什么是函數(shù)猪钮?

簡(jiǎn)要的數(shù)學(xué)回顧

我知道我曾說(shuō)過(guò)品山,離數(shù)學(xué)越遠(yuǎn)越好,但是讓我們暫且忍一小段時(shí)間烤低,在這段時(shí)間里肘交,我們會(huì)盡快地回顧在代數(shù)中一些函數(shù)和圖像的基本知識(shí)。

你還記得你在學(xué)校里學(xué)習(xí)任何有關(guān) f(x) 的知識(shí)嗎扑馁?還有方程 y = f(x) ?

現(xiàn)有方程式定義如下:<code>f(x) = 2x2 + 3</code>涯呻。這個(gè)方程有什么意義凉驻?它對(duì)應(yīng)的圖像是什么樣的呢?如下圖:

你可以注意到:對(duì)于 x 取任意值复罐,例如 2沿侈,帶入方程后會(huì)得到 11。這里的 11 代表函數(shù)的返回值市栗,更簡(jiǎn)單來(lái)說(shuō)就是 y 值缀拭。

根據(jù)上述,現(xiàn)在有一個(gè)點(diǎn) (2,11) 在圖像的曲線上填帽,并且當(dāng)我們有一個(gè) x 值蛛淋,我們都能獲得一個(gè)對(duì)應(yīng)的 y 值。把兩個(gè)值組合就能得到一個(gè)點(diǎn)的坐標(biāo)篡腌,例如 (0,3)褐荷, (-1,5)。當(dāng)把所有的這些點(diǎn)放在一起嘹悼,就會(huì)獲得這個(gè)拋物線方程的圖像叛甫,如上圖所示。

所以杨伙,這些和函數(shù)式編程有什么關(guān)系其监?

在數(shù)學(xué)中,函數(shù)總是獲取一些輸入值限匣,然后給出一個(gè)輸出值抖苦。你能聽(tīng)到一個(gè)函數(shù)式編程的術(shù)語(yǔ)叫做“態(tài)射”:這是一個(gè)優(yōu)雅的方式來(lái)描述一組值和另一組值的映射關(guān)系,就像一個(gè)函數(shù)的輸入值與輸出值之間的關(guān)聯(lián)關(guān)系米死。

在代數(shù)數(shù)學(xué)中锌历,那些輸入值和輸出值經(jīng)常代表著繪制坐標(biāo)的一部分。不過(guò)峦筒,在我們的程序中究西,我們可以定義函數(shù)有各種的輸入和輸出值,并且它們不需要和繪制在圖表上的曲線有任何關(guān)系物喷。

函數(shù) vs 程序

為什么所有的討論都圍繞數(shù)學(xué)和圖像卤材?因?yàn)樵谀撤N程度上,函數(shù)式編程就是使用在數(shù)學(xué)意義上的方程作為函數(shù)脯丝。

你可能會(huì)習(xí)以為常地認(rèn)為函數(shù)就是程序商膊。它們之間的區(qū)別是什么?程序就是一個(gè)任意的功能集合宠进。它或許有許多個(gè)輸入值晕拆,或許沒(méi)有。它或許有一個(gè)輸出值( return 值),或許沒(méi)有实幕。

而函數(shù)則是接收輸入值吝镣,并明確地 return 值。

如果你計(jì)劃使用函數(shù)式編程昆庇,你應(yīng)該盡可能多地使用函數(shù)末贾,而不是程序。你所有編寫(xiě)的 function 應(yīng)該接收輸入值整吆,并且返回輸出值拱撵。這么做的原因是多方面的,我們將會(huì)在后面的書(shū)中來(lái)介紹的表蝙。

函數(shù)輸入

從上述的定義出發(fā)拴测,所有的函數(shù)都需要輸入。

你有時(shí)聽(tīng)人們把函數(shù)的輸入值稱為 “arguments” 或者 “parameters” 府蛇。所以它到底是什么集索?

arguments 是你輸入的值(實(shí)參), parameters 是函數(shù)中的命名變量(形參)汇跨,用于接收函數(shù)的輸入值务荆。例子如下:

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

var a = 3;

foo( a, a * 2 );

aa * 2(即為 6)是函數(shù) foo(..) 調(diào)用的 argumentsxyparameters穷遂,用于接收參數(shù)值(分別為 36 )函匕。

注意: 在 JavaScript 中,實(shí)參的個(gè)數(shù)沒(méi)必要完全符合形參的個(gè)數(shù)塞颁。如果你傳入許多個(gè)實(shí)參浦箱,而且多過(guò)你所聲明的形參吸耿,這些值仍然會(huì)原封不動(dòng)地被傳入祠锣。你可以通過(guò)不同的方式去訪問(wèn),包含了你以前可能聽(tīng)過(guò)的老辦法 —— arguments 對(duì)象咽安。反之伴网,你傳入少于聲明形參個(gè)數(shù)的實(shí)參,所有缺少的參數(shù)將會(huì)被賦予 undefined 變量妆棒,意味著你仍然可以在函數(shù)作用域中使用它澡腾,但值是 undefined

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

一個(gè)函數(shù)所“期望”的實(shí)參個(gè)數(shù)是取決于已聲明的形參個(gè)數(shù)糕珊,即你希望傳入多少參數(shù)动分。

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

foo(..) 期望三個(gè)實(shí)參,因?yàn)樗暶髁巳齻€(gè)形參红选。這里有一個(gè)特殊的術(shù)語(yǔ):Arity澜公。Arity 指的是一個(gè)函數(shù)聲明的形參數(shù)量。 foo(..) 的 Arity 是 3喇肋。

你可能需要在程序運(yùn)行時(shí)獲取函數(shù)的 Arity坟乾,使用函數(shù)的 length 屬性即可迹辐。

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

foo.length;             // 3

在執(zhí)行時(shí)要確定 Arity 的一個(gè)原因是:一段代碼接受一個(gè)函數(shù)的指針引用,有可能這個(gè)引用指向不同來(lái)源甚侣,我們要根據(jù)這些來(lái)源的 Arity 傳入不同的參數(shù)值明吩。

舉個(gè)例子,如果 fn 可能指向的函數(shù)分別期望 1殷费、2 或 3 個(gè)參數(shù)印荔,但你只希望把變量 x 放在最后的位置傳入:

// fn 是一些函數(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 屬性是一個(gè)只讀屬性,并且它是在最初聲明函數(shù)的時(shí)候就被確定了详羡。它應(yīng)該當(dāng)做用來(lái)描述如何使用該函數(shù)的一個(gè)基本元數(shù)據(jù)躏鱼。

需要注意的是,某些參數(shù)列表的變量會(huì)讓 length 屬性變得不同于你的預(yù)期殷绍。別緊張,我們將會(huì)在后續(xù)的章節(jié)逐一解釋這些特性(引入 ES6):

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

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

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

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

如果你使用這些形式的參數(shù)主到,你或許會(huì)被函數(shù)的 length 值嚇一跳。

那我們?cè)趺吹玫疆?dāng)前函數(shù)調(diào)用時(shí)所接收到的實(shí)參個(gè)數(shù)呢畔师?這在以前非常簡(jiǎn)單牧牢,但現(xiàn)在情況稍微復(fù)雜了一些。每一個(gè)函數(shù)都有一個(gè) arguments 對(duì)象(類數(shù)組)存放需要傳入的參數(shù)塔鳍。你可以通過(guò) argumentslength 值來(lái)找出有多少傳入的參數(shù):

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

foo( 3, 4 );

由于 ES5(特別是嚴(yán)格模式下)的 arguments 不被一些人認(rèn)同,很多人盡可能地避免使用腔寡。盡管如此掌唾,它永遠(yuǎn)不會(huì)被移除,這是因?yàn)樵?JS 中我們“永遠(yuǎn)不會(huì)”因?yàn)楸憷远奚蚝蟮募嫒菪耘幢颍疫€是強(qiáng)烈建議不要去使用它凭语。

然而,當(dāng)你需要知道參數(shù)個(gè)數(shù)的時(shí)候撩扒,arguments.length 還是可以用的似扔。在未來(lái)版本的 JS 或許會(huì)新增特性來(lái)替代 arguments.length,如果成真,那么我們可以完全把 arguments 拋諸腦后虫几。

請(qǐng)注意:不要通過(guò) arguments[1] 訪問(wèn)參數(shù)的位置锤灿。只要記住 arguments.length

除此之外辆脸,你或許想知道如何訪問(wèn)那些超出聲明的參數(shù)但校?這個(gè)問(wèn)題我一會(huì)兒會(huì)告訴你,不過(guò)你先要問(wèn)自己的問(wèn)題是啡氢,“為什么我想要知道這個(gè)状囱?”。認(rèn)真地思考一段時(shí)間倘是。

發(fā)生這種情況應(yīng)該是非常罕見(jiàn)的亭枷。因?yàn)檫@不會(huì)是你日常需要的,也不會(huì)是你編寫(xiě)函數(shù)時(shí)所必要的東西搀崭。如果這種情況真的發(fā)生,你應(yīng)該花 20 分鐘來(lái)試著重新設(shè)計(jì)函數(shù)瘤睹,或者命名那些多出來(lái)的參數(shù)轰传。

帶有可變數(shù)量參數(shù)的函數(shù)被稱為 variadic获茬。有些人更喜歡這樣的函數(shù)設(shè)計(jì),不過(guò)你會(huì)發(fā)現(xiàn)鹏氧,這正是函數(shù)式編程者想要避免的度帮。

好了,上面的重點(diǎn)已經(jīng)講得夠多了瞳秽。

例如练俐,當(dāng)你需要像數(shù)組那樣訪問(wèn)參數(shù),很有可能的原因是你想要獲取的參數(shù)沒(méi)有在一個(gè)規(guī)范的位置辜贵。我們?nèi)绾翁幚恚?/p>

ES6 救星來(lái)了托慨!讓我們用 ... 操作符聲明我們的函數(shù)厚棵,也被當(dāng)做 “spread”蔼紧、“rest” 或者 “gather” (我比較偏愛(ài))提及奸例。

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

看到參數(shù)列表中的 ...args 了嗎查吊?那就是 ES6 用來(lái)告訴解析引擎獲取所有剩余的未命名參數(shù)菩貌,并把它們放在一個(gè)真實(shí)的命名為 args 的數(shù)組箭阶。args 無(wú)論是不是空的,它永遠(yuǎn)是一個(gè)數(shù)組嘹叫。但它不包含已經(jīng)命名的 x罩扇,yz 參數(shù)喂饥,只會(huì)包含超出前三個(gè)值的傳入?yún)?shù)员帮。

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 ]

所以捞高,如果你誠(chéng)心想要設(shè)計(jì)一個(gè)函數(shù),并且計(jì)算出任意傳入?yún)?shù)的個(gè)數(shù)氢哮,那就在最后用 ...args (或任何你喜歡的名稱)∪哂龋現(xiàn)在你有一個(gè)真正的生闲、好用的數(shù)組來(lái)獲取這些參數(shù)值了碍讯。

你需要注意的是: 4 所在的位置是 args 的第 0 個(gè),不是在第 3 個(gè)位置马僻。它的 length 值也不包含 123澎埠,...args 剩下所有的值, 但不包括 x蒲稳、yz江耀。

甚至可以直接在參數(shù)列中使用 ... 操作符祥国,沒(méi)有其他正式聲明的參數(shù)也沒(méi)關(guān)系:

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

現(xiàn)在 args 是一個(gè)由參數(shù)組成的完整數(shù)組舌稀,你可以盡情使用 args.length 來(lái)獲取傳入的參數(shù)。你也可以安全地使用 args[1] 或者 args[317]椒惨。當(dāng)然康谆,別真的傳 318 個(gè)參數(shù)沃暗!

說(shuō)到 ES6 的好孽锥,你肯定想知道一些小秘訣惜辑。在這里將會(huì)介紹一些盛撑,更多的內(nèi)容推薦你閱讀《You Don't Know JS: ES6 & Beyond》這本書(shū)的第 2 章捧搞。

關(guān)于實(shí)參的小技巧

如果你希望調(diào)用函數(shù)的時(shí)候只傳一個(gè)數(shù)組代替之前的多個(gè)參數(shù)胎撇,該怎么辦晚树?

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

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

foo( ...arr );                      // 4

我們的新朋友 ... 在這里被使用到了爵憎,但不僅僅在形參列表,在函數(shù)調(diào)用的時(shí)候巡雨,同樣使用在實(shí)參列表铐望。在這里的情況有所不同:在形參列表正蛙,它把實(shí)參整合乒验。在實(shí)參列表锻全,它把實(shí)參展開(kāi)。所以 arr 的內(nèi)容是以函數(shù) foo(..) 引用的單獨(dú)參數(shù)進(jìn)行展開(kāi)荞胡。你能理解傳入一個(gè)引用值和傳入整個(gè) arr 數(shù)組兩者之間的不同了嗎泪漂?

順帶一提萝勤,多個(gè)值和 ... 是可以相互交錯(cuò)放置的呐伞,如下:

var arr = [ 2 ];

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

在對(duì)稱的意義上來(lái)考慮 ... :在值列表的情況荸哟,它會(huì)展開(kāi)鞍历。在賦值的情況,它就像形參列表一樣惧蛹,因?yàn)閷?shí)參會(huì)賦值到形參上香嗓。

無(wú)論采取什么行為靠娱, ... 都會(huì)讓實(shí)參數(shù)組更容易操作像云。那些我們使用實(shí)參數(shù)組 slice(..)迅诬,concat(..)apply(..) 的日子已經(jīng)過(guò)去了侈贷。

關(guān)于形參的小技巧

在 ES6 中等脂,形參可以聲明默認(rèn)值。當(dāng)形參沒(méi)有傳入到實(shí)參中锨并,或者傳入值是 undefined睬棚,會(huì)進(jìn)行默認(rèn)賦值的操作抑党。

思考下面代碼:

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

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

注意: 我們不會(huì)更加詳細(xì)地解釋了底靠,但是默認(rèn)值表達(dá)式是惰性的暑中,這意味著僅當(dāng)需要的時(shí)候鳄逾,它才會(huì)被計(jì)算灵莲。它同樣也可以是一些有效的 JS 表達(dá)式政冻,甚至一個(gè)函數(shù)引用明场。許多非晨嘞牵酷的小技巧用到了這個(gè)方法。例如圾旨,你可以這樣在你的參數(shù)列聲明 x = required()砍的,并且在函數(shù) required()拋出 "This argument is required." 來(lái)確信總有人用你指定的實(shí)參或形參來(lái)引用你的函數(shù)廓鞠。

另一個(gè)我們可以在參數(shù)中使用的 ES6 技巧,被稱為“解構(gòu)”滋早。在這里我們只會(huì)簡(jiǎn)單一提杆麸,因?yàn)橐f(shuō)清這個(gè)話題實(shí)在太過(guò)繁雜昔头。在這里推薦《ES6 & Beyond》這本書(shū)了解更多信息揭斧。

還記得我們之前提到的可以接受 318 個(gè)參數(shù)的 foo(..) 嗎峻堰?

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

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

如果我們想要把函數(shù)內(nèi)的參數(shù)從一個(gè)個(gè)單獨(dú)的參數(shù)值替換為一個(gè)數(shù)組捐名,應(yīng)該怎么做桐筏?這里有兩個(gè) ... 的寫(xiě)法:

function foo(args) {
    // ..
}

foo( [1,2,3] );

這個(gè)非常簡(jiǎn)單梅忌。但如果我們想要命名傳入數(shù)組的第 1牧氮、2 個(gè)值,該怎么做丹莲?我們不能用單獨(dú)傳入?yún)?shù)的辦法了甥材,所以這似乎看起來(lái)無(wú)能為力洲赵。不過(guò)解構(gòu)可以回答這個(gè)問(wèn)題:

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

foo( [1,2,3] );

你看到了在參數(shù)列出現(xiàn)的 [ .. ] 了嗎叠萍?這就是數(shù)組解構(gòu)苛谷。解構(gòu)是通過(guò)你期望的模式來(lái)描述數(shù)據(jù)(對(duì)象腹殿,數(shù)組等),并分配(賦值)值的一種方式绵患。

在這里例子中,解構(gòu)告訴解析器暂幼,一個(gè)數(shù)組應(yīng)該出現(xiàn)的賦值位置(即參數(shù))旺嬉。這種模式是:拿出數(shù)組中的第一個(gè)值厨埋,并且賦值給局部參數(shù)變量 x荡陷,第二個(gè)賦值給 y废赞,剩下的則組成 args唉地。

你可以通過(guò)自己手動(dòng)處理達(dá)到同樣的效果:

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

    // ..
}

現(xiàn)在我們可以發(fā)現(xiàn),在我們這本書(shū)中要多次提到的第一條原則:聲明性代碼通常比命令式代碼更干凈极颓。

聲明式代碼菠隆,如同之前代碼片段里的解構(gòu)浸赫,強(qiáng)調(diào)一段代碼的輸出結(jié)果既峡。命令式代碼运敢,像剛才我們自己手動(dòng)賦值的例子传惠,注重的是如何得到結(jié)果。如果你稍晚再讀這一段代碼羊瘩,你必須在腦子里面再執(zhí)行一遍才能得到你想要的結(jié)果尘吗。這個(gè)結(jié)果是編寫(xiě)在這兒睬捶,但是不是直接可見(jiàn)的擒贸。

只要可能介劫,無(wú)論我們的語(yǔ)言和我們的庫(kù)或框架允許我們達(dá)到什么程度疆拘,我們都應(yīng)該盡可能使用聲明性的和自解釋的代碼哎迄。

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

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

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

我們傳入一個(gè)對(duì)象作為一個(gè)參數(shù)旨涝,它解構(gòu)成兩個(gè)獨(dú)立的參數(shù)變量 xy,從傳入的對(duì)象中分配相應(yīng)屬性名的值贩耐。我們不在意屬性值 x 到底存不存在對(duì)象上潮太,如果不存在铡买,它最終會(huì)如你所想被賦值為 undefined霎箍。

但是我希望你注意:對(duì)象解構(gòu)的部分參數(shù)是將要傳入 foo(..) 的對(duì)象漂坏。

現(xiàn)在有一個(gè)正撤兀可用的調(diào)用現(xiàn)場(chǎng) foo(undefined,3)筋夏,它用于映射實(shí)參到形參条篷。我們?cè)囍?3 放到第二個(gè)位置赴叹,分配給 y乞巧。但是在新的調(diào)用現(xiàn)場(chǎng)上用到了參數(shù)解構(gòu)绽媒,一個(gè)簡(jiǎn)單的對(duì)象屬性代表了實(shí)參 3 應(yīng)該分配給形參(y)免猾。

我們不需要操心 x 應(yīng)該放在哪個(gè)調(diào)用現(xiàn)場(chǎng)猎提。因?yàn)槭聦?shí)上,我們不用去關(guān)心 x棺聊,我們只需要省略它限佩,而不是分配 undefined 值犀暑。

有一些語(yǔ)言對(duì)這樣的操作有一個(gè)直接的特性:命名參數(shù)耐亏。換句話說(shuō)广辰,在調(diào)用現(xiàn)場(chǎng)主之,通過(guò)標(biāo)記輸入值來(lái)告訴它映射關(guān)系槽奕。JavaScript 沒(méi)有命名參數(shù)粤攒,不過(guò)退而求其次夯接,參數(shù)對(duì)象解構(gòu)是一個(gè)選擇。

使用對(duì)象解構(gòu)來(lái)傳入多個(gè)匿名參數(shù)是函數(shù)式編程的優(yōu)勢(shì)晴弃,這個(gè)優(yōu)勢(shì)在于使用一個(gè)參數(shù)(對(duì)象)的函數(shù)能更容易接受另一個(gè)函數(shù)的單個(gè)輸出上鞠。這點(diǎn)會(huì)在后面討論到旗国。

回想一下能曾,術(shù)語(yǔ) Arity 是指期望函數(shù)接收多少個(gè)參數(shù)。Arity 為 1 的函數(shù)也被稱為一元函數(shù)蕊程。在函數(shù)式編程中藻茂,我們希望我們的函數(shù)在任何的情況下是一元的辨赐,有時(shí)我們甚至?xí)褂酶鞣N技巧來(lái)將高 Arity 的函數(shù)都轉(zhuǎn)換為一元的形式掀序。

注意: 在第 3 章惭婿,我們將重新討論命名參數(shù)的解構(gòu)技巧财饥,并使用它來(lái)處理關(guān)于參數(shù)排序的問(wèn)題钥星。

隨著輸入而變化的函數(shù)

思考以下函數(shù)

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

明顯地谦炒,這個(gè)函數(shù)會(huì)根據(jù)你傳入的值而有所不同编饺。

舉例:

foo( 3, 4 );            // 12

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

程序員這樣定義函數(shù)的原因之一是透且,更容易通過(guò)同一個(gè)函數(shù)來(lái)重載不同的功能秽誊。最廣為人知的例子就是 jQuery 提供的 $(..)锅论。"$" 函數(shù)大約有十幾種不同的功能 —— 從 DOM 元素查找最易,到 DOM 元素創(chuàng)建,到等待 “DOMContentLoaded” 事件后剔猿,執(zhí)行一個(gè)函數(shù)归敬,這些都取決于你傳遞給它的參數(shù)鄙早。

上述函數(shù)限番,顯而易見(jiàn)的優(yōu)勢(shì)是 API 變少了(僅僅是一個(gè) $(..) 函數(shù))扳缕,但缺點(diǎn)體現(xiàn)在閱讀代碼上躯舔,你必須仔細(xì)檢查傳遞的內(nèi)容粥庄,理解一個(gè)函數(shù)調(diào)用將做什么。

通過(guò)不同的輸入值讓一個(gè)函數(shù)重載擁有不同的行為的技巧叫做特定多態(tài)(ad hoc polymorphism)布讹。

這種設(shè)計(jì)模式的另一個(gè)表現(xiàn)形式就是在不同的情況下描验,使函數(shù)具有不同的輸出(在下一章節(jié)會(huì)提到)膘流。

警告: 要對(duì)方便的誘惑有警惕之心呼股。因?yàn)槟憧梢酝ㄟ^(guò)這種方式設(shè)計(jì)一個(gè)函數(shù)彭谁,即使可以立即使用缠局,但這個(gè)設(shè)計(jì)的長(zhǎng)期成本可能會(huì)讓你后悔。

函數(shù)輸出

在 JavaScript 中逞度,函數(shù)只會(huì)返回一個(gè)值档泽。下面的三個(gè)函數(shù)都有相同的 return 操作揖赴。

function foo() {}

function bar() {
    return;
}

function baz() {
    return undefined;
}

如果你沒(méi)有 return 值燥滑,或者你使用 return;铭拧,那么則會(huì)隱式地返回 undefined 值搀菩。

如果想要盡可能靠近函數(shù)式編程的定義:使用函數(shù)而非程序,那么我們的函數(shù)必須永遠(yuǎn)有返回值歧蒋。這也意味著他們必須明確地 return 一個(gè)值谜洽,通常這個(gè)值也不是 undefined阐虚。

一個(gè) return 的表達(dá)式僅能夠返回一個(gè)值敌呈。所以,如果你需要返回多個(gè)值吭练,切實(shí)可行的辦法就是把你需要返回的值放到一個(gè)復(fù)合值當(dāng)中去鲫咽,例如數(shù)組、對(duì)象:

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

解構(gòu)方法可以使用于解構(gòu)對(duì)象或者數(shù)組類型的參數(shù)锦聊,也可以使用在平時(shí)的賦值當(dāng)中:

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)回不同的值圆到,這無(wú)形中讓一個(gè)函數(shù)能有多個(gè)輸出結(jié)果芽淡。

提示: 在這里我十分建議你花一點(diǎn)時(shí)間來(lái)思考:是否需要避免函數(shù)有可重構(gòu)的多個(gè)輸出挣菲?或許將這個(gè)函數(shù)分為兩個(gè)或更多個(gè)更小的單用途函數(shù)。有時(shí)會(huì)需要這么做椭赋,有時(shí)可能不需要纹份,但你應(yīng)該至少考慮一下蔓涧。

提前 return

return 語(yǔ)句不僅僅是從函數(shù)中返回一個(gè)值元暴,它也是一個(gè)流量控制結(jié)構(gòu)茉盏,它可以結(jié)束函數(shù)的執(zhí)行枢冤。因此淹真,具有多個(gè) return 語(yǔ)句的函數(shù)具有多個(gè)可能的退出點(diǎ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;
}

突擊測(cè)驗(yàn):不要作弊也不要在瀏覽器中運(yùn)行這段代碼宅楞,請(qǐng)思考 foo(2) 返回什么咱筛? foo(4) 返回什么迅箩? foo(8)foo(12) 呢拐揭?

你對(duì)自己的回答有多少信心堂污?你付出多少精力來(lái)獲得答案盟猖?我錯(cuò)了兩次后式镐,我試圖仔細(xì)思考并且寫(xiě)下來(lái)娘汞!

我認(rèn)為在許多可讀性的問(wèn)題上你弦,是因?yàn)槲覀儾粌H使用 return 返回不同的值禽作,更把它作為一個(gè)流控制結(jié)構(gòu)——在某些情況下可以提前退出一個(gè)函數(shù)的執(zhí)行揩页。我們顯然有更好的方法來(lái)編寫(xiě)流控制( if 邏輯等),也有辦法使輸出路徑更加明顯。

注意: 突擊測(cè)驗(yàn)的答案是:2尘喝,2朽褪,813无虚。

思考以下版本的代碼:

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ú)疑問(wèn)是更冗長(zhǎng)的嗤堰。但是在邏輯上度宦,我認(rèn)為這比上面的代碼更容易理解戈抄。因?yàn)樵诿總€(gè) retValue 可以被設(shè)置的分支划鸽, 這里都有個(gè)守護(hù)者以確保 retValue 沒(méi)有被設(shè)置過(guò)才執(zhí)行裸诽。

相比在函數(shù)中提早使用 return,我們更應(yīng)該用常用的流控制( if 邏輯 )來(lái)控制 retValue 的賦值尸折。到最后实夹,我們 return retValue亮航。

我不是說(shuō)缴淋,你只能有一個(gè) return重抖,或你不應(yīng)該提早 return钟沛,我只是認(rèn)為在定義函數(shù)時(shí),最好不要用 return 來(lái)實(shí)現(xiàn)流控制叁扫,這樣會(huì)創(chuàng)造更多的隱含意義莫绣。嘗試找出最明確的表達(dá)邏輯的方式对室,這往往是最好的辦法软驰。

return 的輸出

有個(gè)技巧你可能在你的大多數(shù)代碼里面使用過(guò)锭亏,并且有可能你自己并沒(méi)有特別意識(shí)到慧瘤,那就是讓一個(gè)函數(shù)通過(guò)改變函數(shù)體外的變量產(chǎn)出一些值锅减。

還記得我們之前提到的函數(shù)<code>f(x) = 2x2 + 3</code>嗎伐坏?我們可以在 JS 中這樣定義:

var y;

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

foo( 2 );

y;                      // 11

我知道這是一個(gè)無(wú)聊的例子桦沉。我們完全可以用 return 來(lái)返回纯露,而不是賦值給 y

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

var y = foo( 2 );

y;                      // 11

這兩個(gè)函數(shù)完成相同的任務(wù)埠褪。我們有什么理由要從中挑一個(gè)嗎?是的贷掖,絕對(duì)有苹威。

解釋這兩者不同的一種方法是屠升,后一個(gè)版本中的 return 表示一個(gè)顯式輸出腹暖,而前者的 y 賦值是一個(gè)隱式輸出脏答。在這種情況下殖告,你可能已經(jīng)猜到了:通常黄绩,開(kāi)發(fā)人員喜歡顯式模式而不是隱式模式玷过。

但是辛蚊,改變一個(gè)外部作用域的變量袋马,就像我們?cè)?foo(..) 中所做的賦值 y 一樣虑凛,只是實(shí)現(xiàn)隱式輸出的一種方式。一個(gè)更微妙的例子是通過(guò)引用對(duì)非局部值進(jìn)行更改墓懂。

思考:

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ù)輸出為 124闪唆,我們也非常明確地 return 了钓葫。但你是否發(fā)現(xiàn)其他的輸出础浮?查看代碼豆同,并檢查 nums 數(shù)組影锈。你發(fā)現(xiàn)區(qū)別了嗎?

為了填補(bǔ) 4 位置的空值 undefined枣抱,這里使用了 0 代替佳晶。盡管我們?cè)诰植坎僮?list 參數(shù)變量宵晚,但我們?nèi)匀挥绊懥送獠康臄?shù)組淤刃。

為什么逸贾?因?yàn)?list 使用了 nums 的引用铝侵,不是對(duì) [1,3,9,..] 的值復(fù)制触徐,而是引用復(fù)制撞鹉。因?yàn)?JS 對(duì)數(shù)組、對(duì)象和函數(shù)都使用引用和引用復(fù)制享郊,我們可以很容易地從函數(shù)中創(chuàng)建輸出炊琉,即使是無(wú)心的苔咪。

這個(gè)隱式函數(shù)輸出在函數(shù)式編程中有一個(gè)特殊的名稱:副作用团赏。當(dāng)然,沒(méi)有副作用的函數(shù)也有一個(gè)特殊的名稱:純函數(shù)。我們將在以后的章節(jié)討論這些鸠踪,但關(guān)鍵是我們應(yīng)該喜歡純函數(shù)营密,并且要盡可能地避免副作用评汰。

函數(shù)功能

函數(shù)是可以接受并且返回任何類型的值被去。一個(gè)函數(shù)如果可以接受或返回一個(gè)甚至多個(gè)函數(shù)奖唯,它被叫做高階函數(shù)丰捷。

思考:

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ù)停巷。

一個(gè)高階函數(shù)同樣可以把一個(gè)函數(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ù)的定義示损。函數(shù)式編程者們應(yīng)該學(xué)會(huì)這樣寫(xiě)!

保持作用域

在所有編程始鱼,尤其是函數(shù)式編程中医清,最強(qiáng)大的就是:當(dāng)一個(gè)函數(shù)內(nèi)部存在另一個(gè)函數(shù)的作用域時(shí)卖氨,對(duì)當(dāng)前函數(shù)進(jìn)行操作筒捺。當(dāng)內(nèi)部函數(shù)從外部函數(shù)引用變量系吭,這被稱作閉包肯尺。

實(shí)際上,閉包是它可以記錄并且訪問(wèn)它作用域外的變量槐臀,甚至當(dāng)這個(gè)函數(shù)在不同的作用域被執(zhí)行峰档。

思考:

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

    return fn;
}

var helloFn = foo( "Hello!" );

helloFn();              // Hello!

處于 foo(..) 函數(shù)作用域中的 msg 參數(shù)變量是可以在內(nèi)部函數(shù)中被引用的讥巡。當(dāng) foo(..) 執(zhí)行時(shí)欢顷,并且內(nèi)部函數(shù)被創(chuàng)建抬驴,函數(shù)可以獲取 msg 變量,即使 return 后仍可被訪問(wèn)豌拙。

雖然我們有函數(shù)內(nèi)部引用 helloFn按傅,現(xiàn)在 foo(..) 執(zhí)行后唯绍,作用域應(yīng)該回收况芒,這也意味著 msg 也不存在了绝骚。不過(guò)這個(gè)情況并不會(huì)發(fā)生祠够,函數(shù)內(nèi)部會(huì)因?yàn)殚]包的關(guān)系哪审,將 msg 保留下來(lái)湿滓。只要內(nèi)部函數(shù)(現(xiàn)在被處在不同作用域的 helloFn 引用)存在叽奥, msg 就會(huì)一直被保留朝氓。

讓我們看看閉包作用的一些例子:

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

identify() 函數(shù)內(nèi)部有兩個(gè)閉包變量赵哲,參數(shù) idrandNumber枫夺。

閉包不僅限于獲取變量的原始值:它不僅僅是快照橡庞,而是直接鏈接。你可以更新該值丑勤,并在下次訪問(wèn)時(shí)獲取更新后的值法竞。

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

警告: 我們將在之后的段落中介紹更多爪喘。不過(guò)在這個(gè)例子中秉剑,你需要盡可能避免使用閉包來(lái)記錄狀態(tài)更改(val)稠诲。

如果你需要設(shè)置兩個(gè)輸入臀叙,一個(gè)你已經(jīng)知道劝萤,另一個(gè)還需要后面才能知道,你可以使用閉包來(lái)記錄第一個(gè)輸入值:

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

//我們已經(jīng)分別知道作為第一個(gè)輸入的 10 和 37
var addTo10 = makeAdder( 10 );
var addTo37 = makeAdder( 37 );

// 緊接著跨释,我們指定第二個(gè)參數(shù)
addTo10( 3 );           // 13
addTo10( 90 );          // 100

addTo37( 13 );          // 50

通常鳖谈, sum(..) 函數(shù)會(huì)一起接收 xy 并相加缆娃。但是在這個(gè)例子中贯要,我們接收并且首先記錄(通過(guò)閉包) x 的值郭毕,然后等待 y 被指定显押。

注意: 在連續(xù)函數(shù)調(diào)用中指定輸入,這種技巧在函數(shù)式編程中非常普遍挖息,并且有兩種形式:偏函數(shù)應(yīng)用和柯里化套腹。我們稍后會(huì)在文中深入討論电禀。

當(dāng)然笤休,因?yàn)楹瘮?shù)如果只是 JS 中的值店雅,我們可以通過(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

函數(shù)式編程并不是在我們的代碼中分配或重復(fù) toUpperCase()toLowerCase() 邏輯窍奋,而是鼓勵(lì)我們用優(yōu)雅的封裝方式來(lái)創(chuàng)建簡(jiǎn)單的函數(shù)琳袄。

具體來(lái)說(shuō)挚歧,我們創(chuàng)建兩個(gè)簡(jiǎn)單的一元函數(shù) lower(..)upperFirst(..)滑负,因?yàn)檫@些函數(shù)在我們程序中矮慕,更容易與其他函數(shù)配合使用痴鳄。

提示: 你知道如何讓 upperFirst(..) 使用 lower(..) 嗎缸夹?

我們將在本書(shū)的后續(xù)中大量使用閉包。如果拋開(kāi)整個(gè)編程來(lái)說(shuō)蛇尚,它可能是所有函數(shù)式編程中最重要的基礎(chǔ)顾画。希望你能用得舒服研侣!

句法

在我們函數(shù)入門(mén)開(kāi)始之前庶诡,讓我們花點(diǎn)時(shí)間來(lái)討論它的語(yǔ)法灌砖。

不同于本書(shū)中的許多其他部分,本節(jié)中的討論主要是意見(jiàn)和偏好蘸吓,無(wú)論你是否同意這里提出的觀點(diǎn)或采取相反的觀點(diǎn)库继。這些想法是非常主觀的宪萄,盡管許多人似乎對(duì)此非常執(zhí)著拜英。不過(guò)最終居凶,都由你決定藤抡。

什么是名稱缠黍?

在語(yǔ)法上,函數(shù)聲明需要包含一個(gè)名稱:

function helloMyNameIs() {
    // ..
}

但是函數(shù)表達(dá)式可以命名或者匿名:

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

bar( function(){    // <-- 這就是匿名的!
    // ..
} );

順便說(shuō)一句语泽,匿名的意思是什么湿弦?具體來(lái)說(shuō)颊埃,函數(shù)具有一個(gè) name 的屬性班利,用于保存函數(shù)在語(yǔ)法上設(shè)定名稱的字符串值罗标,例如 "helloMyNameIs""FunctionExpr"积蜻。 這個(gè)name 屬性特別用于 JS 環(huán)境的控制臺(tái)或開(kāi)發(fā)工具竿拆。當(dāng)我們?cè)诙褩\壽E中追蹤(通常來(lái)自異常)時(shí)丙笋,這個(gè)屬性可以列出該函數(shù)御板。

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

如果你曾經(jīng)試著在一個(gè)異常的堆棧軌跡中調(diào)試一個(gè) JS 程序敬鬓,你可能已經(jīng)發(fā)現(xiàn)痛苦了:看到 (anonymous function) 出現(xiàn)钉答。這個(gè)列表?xiàng)l目不給開(kāi)發(fā)人員任何關(guān)于異常來(lái)源路徑的線索希痴。它沒(méi)有給我們開(kāi)發(fā)者提供任何幫助春感。

如果你命名了你的函數(shù)表達(dá)式嫩实,名稱將會(huì)一直被使用甲献。所以如果你使用了一個(gè)良好的名稱 handleProfileClicks 來(lái)取代 foo颂翼,你將會(huì)在堆棧軌跡中獲得更多的信息朦乏。

在 ES6 中呻疹,匿名表達(dá)式可以通過(guò)名稱引用來(lái)獲得名稱刽锤。思考:

var x = function(){};

x.name;         // x

如果解析器能夠猜到你可能希望函數(shù)采用什么名稱,那么它將會(huì)繼續(xù)下去庐氮。

但請(qǐng)注意旭愧,并不是所有的句法形式都可以用名稱引用输枯。最常見(jiàn)的地方是函數(shù)表達(dá)式是函數(shù)調(diào)用的參數(shù):

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

var x = function(){};

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

當(dāng)名稱不能直接從周圍的語(yǔ)法中被推斷時(shí)桃熄,它仍會(huì)是一個(gè)空字符串瞳收。這樣的函數(shù)將在堆棧軌跡中的被報(bào)告為一個(gè) (anonymous function)螟深。

除了調(diào)試問(wèn)題之外烫葬,函數(shù)被命名還有一個(gè)其他好處。首先垢箕,句法名稱(又稱詞匯名)是可以被函數(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){
        // 未綁定的 event
        evt.target.removeEventListener( "click", handleClick, false );

        // ..
    }, false );

在這些情況下萍悴,使用命名函數(shù)的函數(shù)名引用癣诱,是一種有用和可靠的在自身內(nèi)部自引用的方式撕予。

此外,即使在單行函數(shù)的簡(jiǎn)單情況下欠母,命名它們往往會(huì)使代碼更加明了赏淌,從而讓以前沒(méi)有閱讀過(guò)的人更容易閱讀:

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

光看函數(shù) getPreferredName(..) 的代碼六水,并不能很明確告訴我們這里的操作是什么意圖掷贾。但有名稱就可以增加代碼可讀性想帅。

經(jīng)常使用匿名函數(shù)表達(dá)式的另一個(gè)地方是 IIFE (立即執(zhí)行函數(shù)表達(dá)式):

(function(){

    // 我是 IIFE!

})();

你幾乎從沒(méi)看到為 IIFE 函數(shù)來(lái)命名港准,但他們應(yīng)該命名。為什么?我們剛剛提到過(guò)的原因:堆棧軌跡調(diào)試该押,可靠的自我引用和可讀性蚕礼。如果你想不出你的 IIFE 應(yīng)該叫什么奠蹬,請(qǐng)至少使用 IIFE:

(function IIFE(){

    // 現(xiàn)在你真的知道我叫 IIFE!

})();

我有許多個(gè)理由可以解釋命名函數(shù)比匿名函數(shù)更可取。事實(shí)上冀痕,我甚至認(rèn)為匿名函數(shù)都是不可取的言蛇。相比命名函數(shù)腊尚,他們沒(méi)有任何優(yōu)勢(shì)婿斥。

寫(xiě)匿名功能非常容易民宿,因?yàn)槲覀兺耆挥迷谙朊Q這件事上費(fèi)神費(fèi)力像鸡。

誠(chéng)實(shí)來(lái)講,我也像大家一樣在這個(gè)地方犯錯(cuò)。我不喜歡在起名稱這件事上浪費(fèi)時(shí)間。我能想到命名一個(gè)函數(shù)的前 3 或 4 個(gè)名字通常是不好的巢寡。我必須反復(fù)思考這個(gè)命名宾抓。這個(gè)時(shí)候,我寧愿只是用一個(gè)匿名函數(shù)表達(dá)幢泼。

但是缕棵,我們把易寫(xiě)性拿來(lái)與易讀性做交換招驴,這不是一個(gè)好選擇枷畏。因?yàn)閼卸幌霝槟愕暮瘮?shù)命名拥诡,這是常見(jiàn)的使用匿名功能的借口渴肉。

命名所有單個(gè)函數(shù)。如果你對(duì)著你寫(xiě)的函數(shù)批狐,想不出一個(gè)好名稱嚣艇,我明確告訴你食零,那是你并沒(méi)有完全理解這個(gè)函數(shù)的目的——或者來(lái)說(shuō)它的目的太廣泛或太抽象贰谣。你需要重新設(shè)計(jì)功能吱抚,直到它更清楚考廉。從這個(gè)角度說(shuō)昌粤,一個(gè)名稱會(huì)更明白清晰。

從我自己的經(jīng)驗(yàn)中證明凄贩,在思考名稱的過(guò)程中疲扎,我會(huì)更好地了解它评肆,甚至重構(gòu)其設(shè)計(jì)瓜挽,以提高可讀性和可維護(hù)性久橙。這些時(shí)間的投入是值得的淆衷。

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

到目前為止渤弛,我們一直在使用完整的規(guī)范語(yǔ)法功能她肯。但是相信你也對(duì)新的 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 沒(méi)了枝哄,return() 括號(hào)峭范,{} 花括號(hào)和 ; 分號(hào)也是這樣纱控。所有這一切,都是我們與一個(gè)胖箭頭做了交易: =>舶掖。

但還有另一件事我們忽略了眨攘。 你發(fā)現(xiàn)了嗎鲫售?getPreferredName 函數(shù)名也沒(méi)了情竹。

那就對(duì)了匀哄。 => 箭頭函數(shù)是詞法匿名的涎嚼。沒(méi)有辦法合理地為它提供一個(gè)名字法梯。他們的名字可以像常規(guī)函數(shù)一樣被推斷立哑,但是,最常見(jiàn)的函數(shù)表達(dá)式值作為參數(shù)的情況將不會(huì)起任何作用了滥嘴。

假設(shè) person.nicknames 因?yàn)橐恍┰驔](méi)有被定義若皱,一個(gè)異常將會(huì)被拋出走触,意味著這個(gè) (anonymous function) 將會(huì)在追蹤堆棧的最上層互广。啊像樊!

=> 箭頭函數(shù)的匿名性是 => 的阿喀琉斯之踵生棍。這讓我不能遵守剛剛所說(shuō)的命名原則了:閱讀困難涂滴,調(diào)試?yán)щy柔纵,無(wú)法自我引用搁料。

但是进苍,這還不夠糟糕觉啊,要面對(duì)的另一個(gè)問(wèn)題是杠人,如果你的函數(shù)定義有不同的場(chǎng)景宋下,那么你必須要一大堆細(xì)微差別的語(yǔ)句來(lái)實(shí)現(xiàn)学歧。我不會(huì)在這里詳細(xì)介紹所有枝笨,但會(huì)簡(jiǎn)要地說(shuō):

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

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

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

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

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

在函數(shù)式編程中横浑, => 令人興奮的地方在于它幾乎完全遵循函數(shù)的數(shù)學(xué)符號(hào)徙融,特別是像 Haskell 這樣的函數(shù)式編程語(yǔ)言。=> 箭頭函數(shù)語(yǔ)法甚至可以用于數(shù)學(xué)交流萨脑。

我們進(jìn)一步地來(lái)深挖砚哗,我建議使用 => 的論點(diǎn)是砰奕,通過(guò)使用更輕量級(jí)的語(yǔ)法军援,可以減少函數(shù)之間的視覺(jué)邊界胸哥,也讓我們使用偷懶的方式來(lái)使用它空厌,這也是函數(shù)式編程者的另一個(gè)愛(ài)好。

我認(rèn)為大多數(shù)的函數(shù)式編程者都會(huì)對(duì)此睜只眼閉只眼筐钟。他們喜歡匿名函數(shù)篓冲,喜歡簡(jiǎn)潔語(yǔ)法壹将。但是像我之前說(shuō)過(guò)的那樣:這都由你決定诽俯。

注意: 雖然我不喜歡在我的應(yīng)用程序中使用 =>暴区,但我們將在本書(shū)的其余部分多次使用它密任,特別是當(dāng)我們介紹典型的函數(shù)式編程實(shí)戰(zhàn)時(shí)浪讳,它能簡(jiǎn)化、優(yōu)化代碼片段中的空間负溪。不過(guò)济炎,增強(qiáng)或減弱代碼的可讀性也取決你自己做的決定须尚。

來(lái)說(shuō)說(shuō) This ?

如果您不熟悉 JavaScript 中的 this 綁定規(guī)則密幔,我建議去看我寫(xiě)的《You Don't Know JS: this & Object Prototypes》胯甩。 出于這章的需要偎箫,我會(huì)假定你知道在一個(gè)函數(shù)調(diào)用(四種方式之一)中 this 是什么皆串。但是如果你依然對(duì) this 感到迷惑愚战,告訴你個(gè)好消息寂玲,接下來(lái)我們會(huì)總結(jié)在函數(shù)式編程中你不應(yīng)當(dāng)使用 this梗摇。

JavaScript 的 function 有一個(gè) this 關(guān)鍵字伶授,每個(gè)函數(shù)調(diào)用都會(huì)自動(dòng)綁定糜烹。this 關(guān)鍵字有許多不同的方式描述疮蹦,但我更喜歡說(shuō)它提供了一個(gè)對(duì)象上下文來(lái)使該函數(shù)運(yùn)行。

this 是函數(shù)的一個(gè)隱式的輸入?yún)?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ù)當(dāng)中去,同樣的快耿,對(duì)象也可以作為顯式參數(shù)傳入:

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

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

sum( context );

這樣的代碼更簡(jiǎn)單掀亥,在函數(shù)式編程中也更容易處理:當(dāng)顯性輸入值時(shí)铺浇,我們很容易將多個(gè)函數(shù)組合在一起鳍侣, 或者使用下一章輸入適配技巧。然而當(dāng)我們做同樣的事使用隱性輸入時(shí)线衫,根據(jù)不同的場(chǎng)景授账,有時(shí)候會(huì)難處理白热,有時(shí)候甚至不可能做到粗卜。

還有一些技巧续扔,是基于 this 完成的,例如原型授權(quán)(在《this & Object Prototypes》一書(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+ 的實(shí)用工具刨啸,它用來(lái)將屬性從一個(gè)或者多個(gè)源對(duì)象淺拷貝到目標(biāo)對(duì)象: Object.assign( target, source1, ... )设联。

這段代碼的作用是:現(xiàn)在我們有兩個(gè)獨(dú)立的對(duì)象 LoginAuth仑荐,其中 Login 執(zhí)行原型授權(quán)給 Auth粘招。通過(guò)委托和隱式的 this 共享上下文對(duì)象,這兩個(gè)對(duì)象在 this.authorize() 函數(shù)調(diào)用期間實(shí)際上是組合的辑甜,所以這個(gè) this 上的屬性或方法可以與 Auth.authorize(..) 動(dòng)態(tài)共享 this磷醋。

this 因?yàn)楦鞣N原因邓线,不符合函數(shù)式編程的原則骇陈。其中一個(gè)明顯的問(wèn)題是隱式 this 共享你雌。但我們可以更加顯式地婿崭,更靠向函數(shù)式編程的方向:

// ..

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

// ..

從我的角度來(lái)看氓栈,問(wèn)題不在于使用對(duì)象來(lái)進(jìn)行操作颤绕,而是我們?cè)噲D使用隱式輸入取代顯式輸入祟身。當(dāng)我戴上名為函數(shù)式編程的帽子時(shí)袜硫,我應(yīng)該把 this 放回衣架上婉陷。

總結(jié)

函數(shù)是強(qiáng)大的。

現(xiàn)在闯睹,讓我們清楚地理解什么是函數(shù):它不僅僅是一個(gè)語(yǔ)句或者操作的集合楼吃,而且需要一個(gè)或多個(gè)輸入(理想情況下只需一個(gè):⑽)和一個(gè)輸出亥贸。

函數(shù)內(nèi)部的函數(shù)可以取到閉包外部變量荣挨,并記住它們以備日后使用垦沉。這是所有程序設(shè)計(jì)中最重要的概念之一厕倍,也是函數(shù)式編程的基礎(chǔ)贩疙。

要警惕匿名函數(shù)组民,特別是 => 箭頭函數(shù)臭胜。雖然在編程時(shí)用起來(lái)很方便耸三,但是會(huì)對(duì)增加代碼閱讀的負(fù)擔(dān)仪壮。我們學(xué)習(xí)函數(shù)式編程的全部理由是為了書(shū)寫(xiě)更具可讀性的代碼积锅,所以不要趕時(shí)髦去用匿名函數(shù)。

別用 this 敏感的函數(shù)适篙。這不需要理由匙瘪。

** 【上一章】翻譯連載 |《JavaScript 輕量級(jí)函數(shù)式編程》- 第 1 章:為什么使用函數(shù)式編程? **


活動(dòng)報(bào)名地址:http://www.huodongxing.com/event/5399668594900

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蝶缀,一起剝皮案震驚了整個(gè)濱河市丹喻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌翁都,老刑警劉巖碍论,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異柄慰,居然都是意外死亡藏研,警方通過(guò)查閱死者的電腦和手機(jī)蠢挡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)勤家,“玉大人,你說(shuō)我怎么就攤上這事讼庇。” “怎么了介汹?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)叹卷。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么开瞭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮递宅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘土榴。我一直安慰自己,他們只是感情好矢赁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著额获,像睡著了一般耘眨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上偶宫,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音结闸,去河邊找鬼。 笑死结耀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的黑毅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼易结!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起玉控,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤碌识,失蹤者是張志新(化名)和其女友劉穎开泽,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體峦耘,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡少梁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了挺举。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖绊困,靈堂內(nèi)的尸體忽然破棺而出秤朗,到底是詐尸還是另有隱情常挚,我是刑警寧澤奄毡,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響斤葱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜衩茸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一囊蓝、第九天 我趴在偏房一處隱蔽的房頂上張望狡恬。 院中可真熱鬧,春花似錦兔乞、人聲如沸熊榛。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)择懂。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間要糊,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谈跛。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓令花,卻偏偏與公主長(zhǎng)得像嫂沉,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

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