翻譯連載 | JavaScript 輕量級函數式編程-第2章:函數基礎 |《你不知道的JS》姊妹篇

關於譯者:這是一個流淌着滬江血液的純粹工程:認真,是 HTML 最堅實的樑柱;分享,是 CSS 裏最閃耀的一瞥;總結,是 JavaScript 中最嚴謹的邏輯。通過捶打磨練,成就了本書的中文版。本書包含了函數式編程之精髓,但願能夠幫助你們在學習函數式編程的道路上走的更順暢。比心。前端

譯者團隊(排名不分前後):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry蘿蔔vavd317vivaxy萌萌zhouyaogit

第 2 章:函數基礎

函數式編程不是僅僅用 function 這個關鍵詞來編程。若是真這麼簡單,那我這本書能夠到此爲止了!重點在於:函數函數式編程的核心。這也是如何使用函數(function)才能使咱們的代碼具備函數式(functional)的方法。程序員

然而,你真的明白函數的含義嗎?github

在這一章,咱們將會介紹函數的基礎知識,爲閱讀本書的後續章節打下基礎。從某些方面來說,這章回顧的函數知識並非針對函數式編程者,非函數式編程者一樣須要瞭解。但若是咱們想要充分、全面地學習函數式編程的概念,咱們須要從裏到外地理解函數。編程

請作好準備,由於還有好多你未知的函數知識。設計模式

什麼是函數?

針對函數式編程,很天然而然的我會想到從函數開始。這太明顯不過了,可是我認爲咱們須要紮實地走好旅程的第一步。數組

因此......什麼是函數?瀏覽器

簡要的數學回顧

我知道我曾說過,離數學越遠越好,可是讓咱們暫且忍一小段時間,在這段時間裏,咱們會盡快地回顧在代數中一些函數和圖像的基本知識。安全

你還記得你在學校裏學習任何有關 f(x) 的知識嗎?還有方程 y = f(x) ?閉包

現有方程式定義以下:f(x) = 2x2 + 3。這個方程有什麼意義?它對應的圖像是什麼樣的呢?以下圖:

你能夠注意到:對於 x 取任意值,例如 2,帶入方程後會獲得 11。這裏的 11 表明函數的返回值,更簡單來講就是 y 值。

根據上述,如今有一個點 (2,11) 在圖像的曲線上,而且當咱們有一個 x 值,咱們都能得到一個對應的 y 值。把兩個值組合就能獲得一個點的座標,例如 (0,3)(-1,5)。當把全部的這些點放在一塊兒,就會得到這個拋物線方程的圖像,如上圖所示。

因此,這些和函數式編程有什麼關係?

在數學中,函數老是獲取一些輸入值,而後給出一個輸出值。你能聽到一個函數式編程的術語叫作「態射」:這是一個優雅的方式來描述一組值和另外一組值的映射關係,就像一個函數的輸入值與輸出值之間的關聯關係。

在代數數學中,那些輸入值和輸出值常常表明着繪製座標的一部分。不過,在咱們的程序中,咱們能夠定義函數有各類的輸入和輸出值,而且它們不須要和繪製在圖表上的曲線有任何關係。

函數 vs 程序

爲何全部的討論都圍繞數學和圖像?由於在某種程度上,函數式編程就是使用在數學意義上的方程做爲函數

你可能會習覺得常地認爲函數就是程序。它們之間的區別是什麼?程序就是一個任意的功能集合。它或許有許多個輸入值,或許沒有。它或許有一個輸出值( return 值),或許沒有。

而函數則是接收輸入值,並明確地 return 值。

若是你計劃使用函數式編程,你應該儘量多地使用函數,而不是程序。你全部編寫的 function 應該接收輸入值,而且返回輸出值。這麼作的緣由是多方面的,咱們將會在後面的書中來介紹的。

函數輸入

從上述的定義出發,全部的函數都須要輸入。

你有時聽人們把函數的輸入值稱爲 「arguments」 或者 「parameters」 。因此它究竟是什麼?

arguments 是你輸入的值(實參), parameters 是函數中的命名變量(形參),用於接收函數的輸入值。例子以下:

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

var a = 3;

foo( a, a * 2 );
複製代碼

aa * 2(即爲 6)是函數 foo(..) 調用的 argumentsxyparameters,用於接收參數值(分別爲 36 )。

注意: 在 JavaScript 中,實參的個數不必徹底符合形參的個數。若是你傳入許多個實參,並且多過你所聲明的形參,這些值仍然會原封不動地被傳入。你能夠經過不一樣的方式去訪問,包含了你之前可能聽過的老辦法 —— arguments 對象。反之,你傳入少於聲明形參個數的實參,全部缺乏的參數將會被賦予 undefined 變量,意味着你仍然能夠在函數做用域中使用它,但值是 undefined

輸入計數

一個函數所「指望」的實參個數是取決於已聲明的形參個數,即你但願傳入多少參數。

function foo(x,y,z) {
	// ..
}
複製代碼

foo(..) 指望三個實參,由於它聲明瞭三個形參。這裏有一個特殊的術語:Arity。Arity 指的是一個函數聲明的形參數量。 foo(..) 的 Arity 是 3

你可能須要在程序運行時獲取函數的 Arity,使用函數的 length 屬性便可。

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

foo.length;				// 3
複製代碼

在執行時要肯定 Arity 的一個緣由是:一段代碼接受一個函數的指針引用,有可能這個引用指向不一樣來源,咱們要根據這些來源的 Arity 傳入不一樣的參數值。

舉個例子,若是 fn 可能指向的函數分別指望 一、2 或 3 個參數,但你只但願把變量 x 放在最後的位置傳入:

// fn 是一些函數的引用
// x 是存在的值

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

提示: 函數的 length 屬性是一個只讀屬性,而且它是在最初聲明函數的時候就被肯定了。它應該當作用來描述如何使用該函數的一個基本元數據。

須要注意的是,某些參數列表的變量會讓 length 屬性變得不一樣於你的預期。別緊張,咱們將會在後續的章節逐一解釋這些特性(引入 ES6):

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

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

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

foo.length;				// 1
bar.length;				// 1
baz.length;				// 1
複製代碼

若是你使用這些形式的參數,你或許會被函數的 length 值嚇一跳。

那咱們怎麼獲得當前函數調用時所接收到的實參個數呢?這在之前很是簡單,但如今狀況稍微複雜了一些。每個函數都有一個 arguments 對象(類數組)存放須要傳入的參數。你能夠經過 argumentslength 值來找出有多少傳入的參數:

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

foo( 3, 4 );
複製代碼

因爲 ES5(特別是嚴格模式下)的 arguments 不被一些人認同,不少人儘量地避免使用。儘管如此,它永遠不會被移除,這是由於在 JS 中咱們「永遠不會」由於便利性而去犧牲向後的兼容性,但我仍是強烈建議不要去使用它。

然而,當你須要知道參數個數的時候,arguments.length 仍是能夠用的。在將來版本的 JS 或許會新增特性來替代 arguments.length,若是成真,那麼咱們能夠徹底把 arguments 拋諸腦後。

請注意:不要經過 arguments[1] 訪問參數的位置。只要記住 arguments.length

除此以外,你或許想知道如何訪問那些超出聲明的參數?這個問題我一下子會告訴你,不過你先要問本身的問題是,「爲何我想要知道這個?」。認真地思考一段時間。

發生這種狀況應該是很是罕見的。由於這不會是你平常須要的,也不會是你編寫函數時所必要的東西。若是這種狀況真的發生,你應該花 20 分鐘來試着從新設計函數,或者命名那些多出來的參數。

帶有可變數量參數的函數被稱爲 variadic。有些人更喜歡這樣的函數設計,不過你會發現,這正是函數式編程者想要避免的。

好了,上面的重點已經講得夠多了。

例如,當你須要像數組那樣訪問參數,頗有可能的緣由是你想要獲取的參數沒有在一個規範的位置。咱們如何處理?

ES6 救星來了!讓咱們用 ... 操做符聲明咱們的函數,也被當作 「spread」、「rest」 或者 「gather」 (我比較偏心)說起。

function foo(x,y,z,...args) {
	// ..
}
複製代碼

看到參數列表中的 ...args 了嗎?那就是 ES6 用來告訴解析引擎獲取全部剩餘的未命名參數,並把它們放在一個真實的命名爲 args 的數組。args 不管是否是空的,它永遠是一個數組。但它不包含已經命名的 xyz 參數,只會包含超出前三個值的傳入參數。

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 ]
複製代碼

因此,若是你誠心想要設計一個函數,而且計算出任意傳入參數的個數,那就在最後用 ...args (或任何你喜歡的名稱)。如今你有一個真正的、好用的數組來獲取這些參數值了。

你須要注意的是: 4 所在的位置是 args 的第 0 個,不是在第 3 個位置。它的 length 值也不包含 123...args 剩下全部的值, 但不包括 xyz

甚至能夠直接在參數列中使用 ... 操做符,沒有其餘正式聲明的參數也不要緊:

function foo(...args) {
	// ..
}
複製代碼

如今 args 是一個由參數組成的完整數組,你能夠盡情使用 args.length 來獲取傳入的參數。你也能夠安全地使用 args[1] 或者 args[317]。固然,別真的傳 318 個參數!

說到 ES6 的好,你確定想知道一些小祕訣。在這裏將會介紹一些,更多的內容推薦你閱讀《You Don't Know JS: ES6 & Beyond》這本書的第 2 章。

關於實參的小技巧

若是你但願調用函數的時候只傳一個數組代替以前的多個參數,該怎麼辦?

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

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

foo( ...arr );						// 4
複製代碼

咱們的新朋友 ... 在這裏被使用到了,但不只僅在形參列表,在函數調用的時候,一樣使用在實參列表。在這裏的狀況有所不一樣:在形參列表,它把實參整合。在實參列表,它把實參展開。因此 arr 的內容是以函數 foo(..) 引用的單獨參數進行展開。你能理解傳入一個引用值和傳入整個 arr 數組二者之間的不一樣了嗎?

順帶一提,多個值和 ... 是能夠相互交錯放置的,以下:

var arr = [ 2 ];

foo( 1, ...arr, 3, ...[4,5] );		// 4
複製代碼

在對稱的意義上來考慮 ... :在值列表的狀況,它會展開。在賦值的狀況,它就像形參列表同樣,由於實參會賦值到形參上。

不管採起什麼行爲, ... 都會讓實參數組更容易操做。那些咱們使用實參數組 slice(..)concat(..)apply(..) 的日子已通過去了。

關於形參的小技巧

在 ES6 中,形參能夠聲明默認值。當形參沒有傳入到實參中,或者傳入值是 undefined,會進行默認賦值的操做。

思考下面代碼:

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

foo();					// 3
foo( undefined );		// 3
foo( null );			// null
foo( 0 );				// 0
複製代碼

注意: 咱們不會更加詳細地解釋了,可是默認值表達式是惰性的,這意味着僅當須要的時候,它纔會被計算。它一樣也能夠是一些有效的 JS 表達式,甚至一個函數引用。許多很是酷的小技巧用到了這個方法。例如,你能夠這樣在你的參數列聲明 x = required(),而且在函數 required()拋出 "This argument is required." 來確信總有人用你指定的實參或形參來引用你的函數。

另外一個咱們能夠在參數中使用的 ES6 技巧,被稱爲「解構」。在這裏咱們只會簡單一提,由於要說清這個話題實在太過繁雜。在這裏推薦《ES6 & Beyond》這本書瞭解更多信息。

還記得咱們以前提到的能夠接受 318 個參數的 foo(..) 嗎?

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

foo( ...[1,2,3] );
複製代碼

若是咱們想要把函數內的參數從一個個單獨的參數值替換爲一個數組,應該怎麼作?這裏有兩個 ... 的寫法:

function foo(args) {
	// ..
}

foo( [1,2,3] );
複製代碼

這個很是簡單。但若是咱們想要命名傳入數組的第 一、2 個值,該怎麼作?咱們不能用單獨傳入參數的辦法了,因此這彷佛看起來無能爲力。不過解構能夠回答這個問題:

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

foo( [1,2,3] );
複製代碼

你看到了在參數列出現的 [ .. ] 了嗎?這就是數組解構。解構是經過你指望的模式來描述數據(對象,數組等),並分配(賦值)值的一種方式。

在這裏例子中,解構告訴解析器,一個數組應該出現的賦值位置(即參數)。這種模式是:拿出數組中的第一個值,而且賦值給局部參數變量 x,第二個賦值給 y,剩下的則組成 args

你能夠經過本身手動處理達到一樣的效果:

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

	// ..
}
複製代碼

如今咱們能夠發現,在咱們這本書中要屢次提到的第一條原則:聲明性代碼一般比命令式代碼更乾淨。

聲明式代碼,如同以前代碼片斷裏的解構,強調一段代碼的輸出結果。命令式代碼,像剛纔咱們本身手動賦值的例子,注重的是如何獲得結果。若是你稍晚再讀這一段代碼,你必須在腦子裏面再執行一遍才能獲得你想要的結果。這個結果是編寫在這兒,可是不是直接可見的。

只要可能,不管咱們的語言和咱們的庫或框架容許咱們達到什麼程度,咱們都應該儘量使用聲明性的和自解釋的代碼

正如咱們能夠解構的數組,咱們能夠解構的對象參數:

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

foo( {
	y: 3
} );					// undefined 3
複製代碼

咱們傳入一個對象做爲一個參數,它解構成兩個獨立的參數變量 xy,從傳入的對象中分配相應屬性名的值。咱們不在乎屬性值 x 到底存不存在對象上,若是不存在,它最終會如你所想被賦值爲 undefined

可是我但願你注意:對象解構的部分參數是將要傳入 foo(..) 的對象。

如今有一個正常可用的調用現場 foo(undefined,3),它用於映射實參到形參。咱們試着把 3 放到第二個位置,分配給 y。可是在新的調用現場上用到了參數解構,一個簡單的對象屬性表明了實參 3 應該分配給形參(y)。

咱們不須要操心 x 應該放在哪一個調用現場。由於事實上,咱們不用去關心 x,咱們只須要省略它,而不是分配 undefined 值。

有一些語言對這樣的操做有一個直接的特性:命名參數。換句話說,在調用現場,經過標記輸入值來告訴它映射關係。JavaScript 沒有命名參數,不過退而求其次,參數對象解構是一個選擇。

使用對象解構來傳入多個匿名參數是函數式編程的優點,這個優點在於使用一個參數(對象)的函數能更容易接受另外一個函數的單個輸出。這點會在後面討論到。

回想一下,術語 Arity 是指指望函數接收多少個參數。Arity 爲 1 的函數也被稱爲一元函數。在函數式編程中,咱們但願咱們的函數在任何的狀況下是一元的,有時咱們甚至會使用各類技巧來將高 Arity 的函數都轉換爲一元的形式。

注意: 在第 3 章,咱們將從新討論命名參數的解構技巧,並使用它來處理關於參數排序的問題。

隨着輸入而變化的函數

思考如下函數

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

明顯地,這個函數會根據你傳入的值而有所不一樣。

舉例:

foo( 3, 4 );			// 12

foo( "3", 4 );			// "34"
複製代碼

程序員這樣定義函數的緣由之一是,更容易經過同一個函數來重載不一樣的功能。最廣爲人知的例子就是 jQuery 提供的 $(..)。"$" 函數大約有十幾種不一樣的功能 —— 從 DOM 元素查找,到 DOM 元素建立,到等待 「DOMContentLoaded」 事件後,執行一個函數,這些都取決於你傳遞給它的參數。

上述函數,顯而易見的優點是 API 變少了(僅僅是一個 $(..) 函數),但缺點體如今閱讀代碼上,你必須仔細檢查傳遞的內容,理解一個函數調用將作什麼。

經過不一樣的輸入值讓一個函數重載擁有不一樣的行爲的技巧叫作特定多態(ad hoc polymorphism)。

這種設計模式的另外一個表現形式就是在不一樣的狀況下,使函數具備不一樣的輸出(在下一章節會提到)。

警告: 要對方便的誘惑有警戒之心。由於你能夠經過這種方式設計一個函數,即便能夠當即使用,但這個設計的長期成本可能會讓你後悔。

函數輸出

在 JavaScript 中,函數只會返回一個值。下面的三個函數都有相同的 return 操做。

function foo() {}

function bar() {
	return;
}

function baz() {
	return undefined;
}
複製代碼

若是你沒有 return 值,或者你使用 return;,那麼則會隱式地返回 undefined 值。

若是想要儘量靠近函數式編程的定義:使用函數而非程序,那麼咱們的函數必須永遠有返回值。這也意味着他們必須明確地 return 一個值,一般這個值也不是 undefined

一個 return 的表達式僅可以返回一個值。因此,若是你須要返回多個值,切實可行的辦法就是把你須要返回的值放到一個複合值當中去,例如數組、對象:

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

解構方法可使用於解構對象或者數組類型的參數,也可使用在平時的賦值當中:

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

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

將多個值集合成一個數組(或對象)作爲返回值,而後再解構回不一樣的值,這無形中讓一個函數能有多個輸出結果。

提示: 在這裏我十分建議你花一點時間來思考:是否須要避免函數有可重構的多個輸出?或許將這個函數分爲兩個或更多個更小的單用途函數。有時會須要這麼作,有時可能不須要,但你應該至少考慮一下。

提早 return

return 語句不只僅是從函數中返回一個值,它也是一個流量控制結構,它能夠結束函數的執行。所以,具備多個 return 語句的函數具備多個可能的退出點,這意味着若是輸出的路徑不少,可能難以讀取並理解函數的輸出行爲。

思考如下:

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) 呢?

你對本身的回答有多少信心?你付出多少精力來得到答案?我錯了兩次後,我試圖仔細思考而且寫下來!

我認爲在許多可讀性的問題上,是由於咱們不只使用 return 返回不一樣的值,更把它做爲一個流控制結構——在某些狀況下能夠提早退出一個函數的執行。咱們顯然有更好的方法來編寫流控制( if 邏輯等),也有辦法使輸出路徑更加明顯。

注意: 突擊測驗的答案是:22813

思考如下版本的代碼:

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 能夠被設置的分支, 這裏都有個守護者以確保 retValue 沒有被設置過才執行。

相比在函數中提前使用 return,咱們更應該用經常使用的流控制( if 邏輯 )來控制 retValue 的賦值。到最後,咱們 return retValue

我不是說,你只能有一個 return,或你不該該提前 return,我只是認爲在定義函數時,最好不要用 return 來實現流控制,這樣會創造更多的隱含意義。嘗試找出最明確的表達邏輯的方式,這每每是最好的辦法。

return 的輸出

有個技巧你可能在你的大多數代碼裏面使用過,而且有可能你本身並無特別意識到,那就是讓一個函數經過改變函數體外的變量產出一些值。

還記得咱們以前提到的函數f(x) = 2x2 + 3嗎?咱們能夠在 JS 中這樣定義:

var y;

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

foo( 2 );

y;						// 11
複製代碼

我知道這是一個無聊的例子。咱們徹底能夠用 return 來返回,而不是賦值給 y

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

var y = foo( 2 );

y;						// 11
複製代碼

這兩個函數完成相同的任務。咱們有什麼理由要從中挑一個嗎?是的,絕對有。

解釋這二者不一樣的一種方法是,後一個版本中的 return 表示一個顯式輸出,而前者的 y 賦值是一個隱式輸出。在這種狀況下,你可能已經猜到了:一般,開發人員喜歡顯式模式而不是隱式模式。

可是,改變一個外部做用域的變量,就像咱們在 foo(..) 中所作的賦值 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
複製代碼

很明顯,這個函數輸出爲 124,咱們也很是明確地 return 了。但你是否發現其餘的輸出?查看代碼,並檢查 nums 數組。你發現區別了嗎?

爲了填補 4 位置的空值 undefined,這裏使用了 0 代替。儘管咱們在局部操做 list 參數變量,但咱們仍然影響了外部的數組。

爲何?由於 list 使用了 nums 的引用,不是對 [1,3,9,..] 的值複製,而是引用複製。由於 JS 對數組、對象和函數都使用引用和引用複製,咱們能夠很容易地從函數中建立輸出,即便是無意的。

這個隱式函數輸出在函數式編程中有一個特殊的名稱:反作用。固然,沒有反作用的函數也有一個特殊的名稱:純函數。咱們將在之後的章節討論這些,但關鍵是咱們應該喜歡純函數,而且要儘量地避免反作用。

函數功能

函數是能夠接受而且返回任何類型的值。一個函數若是能夠接受或返回一個甚至多個函數,它被叫作高階函數。

思考:

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(..) 就是一個高階函數,由於它能夠接受一個函數做爲參數。

一個高階函數一樣能夠把一個函數做爲輸出,像這樣:

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

	return fn;
}

var f = foo();

f( "Hello!" );			// Hello!
複製代碼

return 不是「輸出」函數的惟一辦法。

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

	bar( fn );
}

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

foo();					// Hello!
複製代碼

將其餘函數視爲值的函數是高階函數的定義。函數式編程者們應該學會這樣寫!

保持做用域

在全部編程,尤爲是函數式編程中,最強大的就是:當一個函數內部存在另外一個函數的做用域時,對當前函數進行操做。當內部函數從外部函數引用變量,這被稱做閉包。

實際上,閉包是它能夠記錄而且訪問它做用域外的變量,甚至當這個函數在不一樣的做用域被執行。

思考:

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

	return fn;
}

var helloFn = foo( "Hello!" );

helloFn();				// Hello!
複製代碼

處於 foo(..) 函數做用域中的 msg 參數變量是能夠在內部函數中被引用的。當 foo(..) 執行時,而且內部函數被建立,函數能夠獲取 msg 變量,即便 return 後仍可被訪問。

雖然咱們有函數內部引用 helloFn,如今 foo(..) 執行後,做用域應該回收,這也意味着 msg 也不存在了。不過這個狀況並不會發生,函數內部會由於閉包的關係,將 msg 保留下來。只要內部函數(如今被處在不一樣做用域的 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
複製代碼

identify() 函數內部有兩個閉包變量,參數 idrandNumber

閉包不只限於獲取變量的原始值:它不只僅是快照,而是直接連接。你能夠更新該值,並在下次訪問時獲取更新後的值。

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
複製代碼

警告: 咱們將在以後的段落中介紹更多。不過在這個例子中,你須要儘量避免使用閉包來記錄狀態更改(val)。

若是你須要設置兩個輸入,一個你已經知道,另外一個還須要後面才能知道,你可使用閉包來記錄第一個輸入值:

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

//咱們已經分別知道做爲第一個輸入的 10 和 37
var addTo10 = makeAdder( 10 );
var addTo37 = makeAdder( 37 );

// 緊接着,咱們指定第二個參數
addTo10( 3 );			// 13
addTo10( 90 );			// 100

addTo37( 13 );			// 50
複製代碼

一般, sum(..) 函數會一塊兒接收 xy 並相加。可是在這個例子中,咱們接收而且首先記錄(經過閉包) x 的值,而後等待 y 被指定。

注意: 在連續函數調用中指定輸入,這種技巧在函數式編程中很是廣泛,而且有兩種形式:偏函數應用和柯里化。咱們稍後會在文中深刻討論。

固然,由於函數若是隻是 JS 中的值,咱們能夠經過閉包來記住函數值。

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() 邏輯,而是鼓勵咱們用優雅的封裝方式來建立簡單的函數。

具體來講,咱們建立兩個簡單的一元函數 lower(..)upperFirst(..),由於這些函數在咱們程序中,更容易與其餘函數配合使用。

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

咱們將在本書的後續中大量使用閉包。若是拋開整個編程來講,它多是全部函數式編程中最重要的基礎。但願你能用得舒服!

句法

在咱們函數入門開始以前,讓咱們花點時間來討論它的語法。

不一樣於本書中的許多其餘部分,本節中的討論主要是意見和偏好,不管你是否贊成這裏提出的觀點或採起相反的觀點。這些想法是很是主觀的,儘管許多人彷佛對此很是執着。不過最終,都由你決定。

什麼是名稱?

在語法上,函數聲明須要包含一個名稱:

function helloMyNameIs() {
	// ..
}
複製代碼

可是函數表達式能夠命名或者匿名:

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

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

順便說一句,匿名的意思是什麼?具體來講,函數具備一個 name 的屬性,用於保存函數在語法上設定名稱的字符串值,例如 "helloMyNameIs""FunctionExpr"。 這個name 屬性特別用於 JS 環境的控制檯或開發工具。當咱們在堆棧軌跡中追蹤(一般來自異常)時,這個屬性能夠列出該函數。

而匿名函數一般顯示爲:(anonymous function)

若是你曾經試着在一個異常的堆棧軌跡中調試一個 JS 程序,你可能已經發現痛苦了:看到 (anonymous function) 出現。這個列表條目不給開發人員任何關於異常來源路徑的線索。它沒有給咱們開發者提供任何幫助。

若是你命名了你的函數表達式,名稱將會一直被使用。因此若是你使用了一個良好的名稱 handleProfileClicks 來取代 foo,你將會在堆棧軌跡中得到更多的信息。

在 ES6 中,匿名錶達式能夠經過名稱引用來得到名稱。思考:

var x = function(){};

x.name;			// x
複製代碼

若是解析器可以猜到你可能但願函數採用什麼名稱,那麼它將會繼續下去。

但請注意,並非全部的句法形式均可以用名稱引用。最多見的地方是函數表達式是函數調用的參數:

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

var x = function(){};

foo( x );				// x
foo( function(){} );	//
複製代碼

當名稱不能直接從周圍的語法中被推斷時,它仍會是一個空字符串。這樣的函數將在堆棧軌跡中的被報告爲一個 (anonymous function)

除了調試問題以外,函數被命名還有一個其餘好處。首先,句法名稱(又稱詞彙名)是能夠被函數內部的自引用。自引用是遞歸(同步和異步)所必需的,也有助於事件處理。

思考這些不一樣的狀況:

// 同步狀況:
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 );
複製代碼

在這些狀況下,使用命名函數的函數名引用,是一種有用和可靠的在自身內部自引用的方式。

此外,即便在單行函數的簡單狀況下,命名它們每每會使代碼更加明瞭,從而讓之前沒有閱讀過的人更容易閱讀:

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

光看函數 getPreferredName(..) 的代碼,並不能很明確告訴咱們這裏的操做是什麼意圖。但有名稱就能夠增長代碼可讀性。

常用匿名函數表達式的另外一個地方是 IIFE (當即執行函數表達式):

(function(){

	// 我是 IIFE!

})();
複製代碼

你幾乎從沒看到爲 IIFE 函數來命名,但他們應該命名。爲何?咱們剛剛提到過的緣由:堆棧軌跡調試,可靠的自我引用和可讀性。若是你想不出你的 IIFE 應該叫什麼,請至少使用 IIFE:

(function IIFE(){

	// 如今你真的知道我叫 IIFE!

})();
複製代碼

我有許多個理由能夠解釋命名函數比匿名函數更可取。事實上,我甚至認爲匿名函數都是不可取的。相比命名函數,他們沒有任何優點。

寫匿名功能很是容易,由於咱們徹底不用在想名稱這件事上費神費力。

誠實來說,我也像你們同樣在這個地方犯錯。我不喜歡在起名稱這件事上浪費時間。我能想到命名一個函數的前 3 或 4 個名字一般是很差的。我必須反覆思考這個命名。這個時候,我寧願只是用一個匿名函數表達。

可是,咱們把易寫性拿來與易讀性作交換,這不是一個好選擇。由於懶而不想爲你的函數命名,這是常見的使用匿名功能的藉口。

命名全部單個函數。若是你對着你寫的函數,想不出一個好名稱,我明確告訴你,那是你並無徹底理解這個函數的目的——或者來講它的目的太普遍或太抽象。你須要從新設計功能,直到它更清楚。從這個角度說,一個名稱會更明白清晰。

從我本身的經驗中證實,在思考名稱的過程當中,我會更好地瞭解它,甚至重構其設計,以提升可讀性和可維護性。這些時間的投入是值得的。

沒有 function 的函數

到目前爲止,咱們一直在使用完整的規範語法功能。可是相信你也對新的 ES6 => 箭頭函數語法有所耳聞。

比較:

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

people.map( person => person.nicknames[0] || person.firstName );
複製代碼

哇!

關鍵字 function 沒了,return() 括號,{} 花括號和 ; 分號也是這樣。全部這一切,都是咱們與一個胖箭頭作了交易: =>

但還有另外一件事咱們忽略了。 你發現了嗎?getPreferredName 函數名也沒了。

那就對了。 => 箭頭函數是詞法匿名的。沒有辦法合理地爲它提供一個名字。他們的名字能夠像常規函數同樣被推斷,可是,最多見的函數表達式值做爲參數的狀況將不會起任何做用了。

假設 person.nicknames 由於一些緣由沒有被定義,一個異常將會被拋出,意味着這個 (anonymous function) 將會在追蹤堆棧的最上層。啊!

=> 箭頭函數的匿名性是 => 的阿喀琉斯之踵。這讓我不能遵照剛剛所說的命名原則了:閱讀困難,調試困難,沒法自我引用。

可是,這還不夠糟糕,要面對的另外一個問題是,若是你的函數定義有不一樣的場景,那麼你必需要一大堆細微差異的語句來實現。我不會在這裏詳細介紹全部,但會簡要地說:

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

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

// 解構參數? 須要 ( )
people.map( ({ person }) => person.nicknames[0] || person.firstName );

// 默認參數? 須要 ( )
people.map( (person = {}) => person.nicknames[0] || person.firstName );

// 返回對象? 須要 ( )
people.map( person =>
	({ preferredName: person.nicknames[0] || person.firstName })
);
複製代碼

在函數式編程中, => 使人興奮的地方在於它幾乎徹底遵循函數的數學符號,特別是像 Haskell 這樣的函數式編程語言。=> 箭頭函數語法甚至能夠用於數學交流。

咱們進一步地來深挖,我建議使用 => 的論點是,經過使用更輕量級的語法,能夠減小函數之間的視覺邊界,也讓咱們使用偷懶的方式來使用它,這也是函數式編程者的另外一個愛好。

我認爲大多數的函數式編程者都會對此睜隻眼閉隻眼。他們喜歡匿名函數,喜歡簡潔語法。可是像我以前說過的那樣:這都由你決定。

注意: 雖然我不喜歡在個人應用程序中使用 =>,但咱們將在本書的其他部分屢次使用它,特別是當咱們介紹典型的函數式編程實戰時,它能簡化、優化代碼片斷中的空間。不過,加強或減弱代碼的可讀性也取決你本身作的決定。

來講說 This ?

若是您不熟悉 JavaScript 中的 this 綁定規則,我建議去看我寫的《You Don't Know JS: this & Object Prototypes》。 出於這章的須要,我會假定你知道在一個函數調用(四種方式之一)中 this 是什麼。可是若是你依然對 this 感到迷惑,告訴你個好消息,接下來咱們會總結在函數式編程中你不該當使用 this

JavaScript 的 function 有一個 this 關鍵字,每一個函數調用都會自動綁定。this 關鍵字有許多不一樣的方式描述,但我更喜歡說它提供了一個對象上下文來使該函數運行。

this 是函數的一個隱式的輸入參數。

思考:

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
複製代碼

固然,若是 this 可以隱式地輸入到一個函數當中去,一樣的,對象也能夠做爲顯式參數傳入:

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

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

sum( context );
複製代碼

這樣的代碼更簡單,在函數式編程中也更容易處理:當顯性輸入值時,咱們很容易將多個函數組合在一塊兒, 或者使用下一章輸入適配技巧。然而當咱們作一樣的事使用隱性輸入時,根據不一樣的場景,有時候會難處理,有時候甚至不可能作到。

還有一些技巧,是基於 this 完成的,例如原型受權(在《this & Object Prototypes》一書中也詳細介紹):

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+ 的實用工具,它用來將屬性從一個或者多個源對象淺拷貝到目標對象: Object.assign( target, source1, ... )

這段代碼的做用是:如今咱們有兩個獨立的對象 LoginAuth,其中 Login 執行原型受權給 Auth。經過委託和隱式的 this 共享上下文對象,這兩個對象在 this.authorize() 函數調用期間其實是組合的,因此這個 this 上的屬性或方法能夠與 Auth.authorize(..) 動態共享 this

this 由於各類緣由,不符合函數式編程的原則。其中一個明顯的問題是隱式 this 共享。但咱們能夠更加顯式地,更靠向函數式編程的方向:

// ..

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

// ..
複製代碼

從個人角度來看,問題不在於使用對象來進行操做,而是咱們試圖使用隱式輸入取代顯式輸入。當我戴上名爲函數式編程的帽子時,我應該把 this 放回衣架上。

總結

函數是強大的。

如今,讓咱們清楚地理解什麼是函數:它不只僅是一個語句或者操做的集合,並且須要一個或多個輸入(理想狀況下只需一個!)和一個輸出。

函數內部的函數能夠取到閉包外部變量,並記住它們以備往後使用。這是全部程序設計中最重要的概念之一,也是函數式編程的基礎。

要警戒匿名函數,特別是 => 箭頭函數。雖然在編程時用起來很方便,可是會對增長代碼閱讀的負擔。咱們學習函數式編程的所有理由是爲了書寫更具可讀性的代碼,因此不要趕時髦去用匿名函數。

別用 this 敏感的函數。這不須要理由。

** 【上一章】翻譯連載 |《JavaScript 輕量級函數式編程》- 第 1 章:爲何使用函數式編程? ** ** 【下一章】翻譯連載 |《JavaScript 輕量級函數式編程》- 第3章:管理函數的輸入 **

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

iKcamp新課程推出啦~~~~~開始免費連載啦~每週2更共11堂iKcamp課|基於Koa2搭建Node.js實戰項目教學(含視頻)| 課程大綱介紹

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息