箭頭函式
ES6篇 - 箭頭函式
本章的目標是對箭頭函式提供一些較為全面性的介紹,除了基本的語法之外,也補充了很多React搭配使用時的實例,此外也提供撰寫的風格建議。當然,箭頭函式並不光只是語法簡短而已,它有一些與原來JavaScript中函式不同的設計,你可以把它當成是原本(傳統)的函式的改進版本。
註: 本文章同步放置於Github庫的這裡。
根據網路上統計的統計資料,箭頭函式(Arrow Functions)是ES6標準中,是最受歡迎的其中一種ES6新特性。它會受歡迎的原因是好處多多,只要注意在某些情況下不要用過頭就行了。有什麼好處呢?大致上有以下幾點:
語法簡單,少打很多字元
可以讓程式碼的可閱讀性提高
可以綁定詞法上的
this
值
註: 上述的統計可以參考2ality與ponyfoo這兩個網站的相關統計資料。
註: 本文大部份的基本語法內容是來自我電子書中的箭頭函式章節。
語法介紹
箭頭函式的語法如下,出自箭頭函數(MDN):
簡單的說明如下:
符號是肥箭頭符號(=>) (註: "->"是瘦箭頭)
基本上是"函式表達式(FE)的簡短寫法"
一個簡單的範例是:
相當於
所以你可以少打很多英文字元與一些標點符號之類的,函式是個匿名的函式。基本上的使用如下說明:
花括號({})是有意義的,如果函式有多行語句(表達式)時就要使用花括號,花括號中的
return
回傳值語句要自己寫。例如() => {}
只有單一個傳入參數時,可以不需要左邊(前面)作為傳入參數使用的圓括號(())符號,例如
x => x*x
因此,初學者最容易搞混的是下面這個例子,因為有花括號({})與沒有是兩碼子事:
當沒使用花括號({})時,代表要會使用自動有return
的作用,所以它也只能用在單一行的表達式的時候使用。使用花括號({})則是可以加入多行的語句,不過return
不會自動加,有需要你要自己加上,沒加這個函式最後等於return undefined
註: 表達式與語句仍然有一些差異,像
throw "Error2"
是一個只有單一行的語句,但它並不能用於箭頭函式中無花括號的情況。註: JS語言中函式的設計,必有回傳值,沒寫相當於回傳undefined
第二個會容易造成混亂與誤解的是,在肥箭頭符號(=>)的後面可以直接換行,下面指的是單行回傳表達式的情況,雖然不會造成錯誤但很難閱讀,所以不建議這樣寫。像下面這幾個的例子都是合法語法:
在這裡容易造成誤解,理由是肥箭頭符號(=>)的後面可以用換行,但"前面"不能直接接到換行,這個會造成編譯器無法編譯,瀏覽器也無法執行。我想主要原因是,畢竟這個符號是等號(=)與大於符號(>)組合而成的一個新符號,等號還有其他的用途。放在語句的前面應該就是等號的作用,再加上一個大於符號會造成語法錯誤。不管如何,別亂寫語法就對了,按照一般的你所看到的正常語法來寫就對了。
通常要換行或是寫成多行的語句時,則是要配合使用花括號或圓括號,但它們兩者在實際上的意義不同。在使用某些函式庫例如React時,你會看到在箭頭後面使用圓括號(())而非花括號({}),會使用圓括號(())會有兩個情況,第一個是仍然是有自動加retrun回傳的作用,但這個只有單一行的表達式在撰寫時區分為多行來寫,第二個是回傳值是個物件的類型值,因為物件的字面文字定義同樣也是用花括號({}),為了明顯區分出是物件的字面文字,所以用圓括號框住,這一點要相當注意。
React中因為使用了JSX語法,所以有可能你會看到一大串的JSX語法最外圍都會加上圓括號(()),例如下面的範例,我這裡只有寫一小串不過通常是一大串:
圓括號在JavaScript中的用途你應該有看過了一些,用於函式呼叫定義,if之類的語句中的表達式,以及它是一個群組運算符(Grouping operator),可以明確地控制運算求值的優先次序,例如((a + b) * c)
。在這裡的用法,則是形成一種未完結的語句,也就是撰寫時多行實際是單一整行的語句,這是由於按照JS中的ASI(Automatic Semicolon Insertion, 自動插入分號)機制,當讀到圓括號的開頭符號((
)會被認為這個語句尚未完成,並不會自動插入分號(;)來作語句的結束,一直到看到圓括號的結尾符號()
),才會整個算作一個語句。這與使用花括號({})作為區塊語句的語法,或是函式的傳入參數寫成多行(下面的範例中就有這樣寫)有點類似,但意義不同。但用在這裡的情況仍然是箭頭函式有自動回傳的情況,也就是仍然屬於單一行的表達式。
不過,JSX語法畢竟只是一種簡寫語法,上面的範例透過babel編譯器後,它的程式碼相於下面這樣:
要補充一點的是,雖然與箭頭函式無關,但在React中也很常見到的一種JSX的寫法,就是使用圓括號開頭符號((
)接在return
關鍵字的後面,尤其是在render
函式中,例如像下面這樣的例子:
它的功能也是類似上面所說的,形成一個未完結的語句,讓JS認為這行語句尚未結束,而不會作ASI的來結束語句。如果return後面沒有加上圓括號的開頭符號((
),ASI會起作用然後會幫你自動加上分號(;),這將會造成語法錯誤或是不預期的結果。這一點要特別的小心注意。
註: 以上關於ASI(Automatic Semicolon Insertion, 自動插入分號)的機制,可以參考這篇我寫的部落格文章。
this
值的綁定
this
值的綁定箭頭函式可以取代某些原有使用self = this
或.bind(this)
的情況,它可以在詞法上綁定this
變數。但要視情況而定,而不是每種情況都一定可以用箭頭函式來取代。下面來簡單說明一下。
箭頭函式為什麼可以綁定this
一開頭要說的是,箭頭函式並不是完全只是原本JS中函式的縮寫語法而已,它與原本的函式內部設計有些不同,稱得上是改進過的函式。
箭頭函式與原本(傳統)的函式的不同之處,以下這兩點簡單說明:
沒有定義本地端綁定(local bindings)的以下幾個變數,也就是說這些變數將由lexical(詞法的)綁定: arguments, super, this, new.target
不能作為建構函式(constructor)使用,不能使用
new
函式在JS中的功用很多,除了像一般的其他程式語言中的函式那樣使用外,函式也可以作為建構函式使用,這是JS中設計出來的四不像的以類別為基礎的物件導向語法,不得不說它又是一個容易造成混亂的設計,這邊我就不再多描述。
函式區塊中還有另一個隱藏的自動生成物件 - arguments。它是一個討論區中的常客,這個物件因為是隱藏的機制,除了是個"偽"陣列的物件,而且它與傳入參數有互相參照的作用,初學者如果用了它就像踏到坑裡面去,不是很容易能掌握它而且會常常出現問題。如果是以我的看法,"不要使用它"就是最佳的回答。連JavaScript的創造者Brendan Eich都認為在ES6之後,這個物件應該逐漸要退出舞台,不需要再使用它。
在ES6之後,JS引進了一種新式的設計,稱為Lexical this
(詞法的this),下面簡單的說明一下。
一般而言,在JS中的預設this值,就是呼叫這個函式的物件,例如是o.m()
,除非m
是一個綁定方法(一般指bind這個方法),不然this預設就是o
。在ES5嚴格模式的作用下,一般的在詞法中函式呼叫(f()
)的this預設都會是undefined
,而不是window或全域物件。但如果預設的this值是undefined
時,它也沒什麼太大的功用。
註: 上面這一段是摘譯Brendan Eich的部落格的。
Lexical this
(詞法的this)是一種預設this的自動機制,這裡所謂的詞法(Lexical)一般指的是在函式區塊中,這因為在函式呼叫時才會產生this。也就是說Lexical this
(詞法的this)是在函式中使用一些特定的語法時,這些語法就會從週邊的作用域(函式或全域作用域)中,捕捉this值,作為自己的預設this。雖然有可能你還搞不清楚,這是在講什麼,但是這的確會非常的有用處。
箭頭函式的預設this就是用Lexical this
(詞法的this),它雖然是函式,但它並沒有像原本(傳統)的函式的設計,在被呼叫時以呼叫它的物件作為預設this值,或是在全域呼叫時以window或全域物件作為預設this(或是嚴格模式下是undefined)。它不是這樣設計的,它是從詞法的作用域中捕捉this值,作為自己的預設this值。
註: 不要把this與scope搞混了,是不一樣的東西。你可以參考這裡書中的this的內容。
註: 雖然說詞法環境(LexicalEnvironment)我們指的就是作用域,而ES6中也有區塊作用域(block scope),但因為這裡討論到的是
this
,會影響到Lexical this
(詞法的this)的只有函式或全域的作用域,其他的區域作用域(例如if, for等等)並沒有影響到。
上面講了一堆,實際看例子就會很容易理解,這是一個在函式中使用箭頭函式的例子:
在setTimeout
中的箭頭函式,可以有效地捕抓到外層的func區塊中的this值,作為自己的預設this。所以當使用obj呼叫func時。箭頭函式中的this.a
可以正確地得到1
。
看起來沒啥大不了,比對一下如果用一般的函式定義會如何?下面是一般的函式定義方式:
在以往這個有名的常見問題中,解決方式有很多種,要不就是要開發者自己作"捕捉"外層函式this的動作,或是用bind
方法自己綁定,下面這是其中一種用"捕捉"外層函式this的解決方式:
註: 上面的程式碼中的
that
是隨便取的名稱,有人喜歡用self
、_self
、_that
都一樣的用途。不要誤以為它是個關鍵字。
類似的問題非常多,你可以從setTimeout
的例子就可以看到,這種用了callback(回調, 回呼)作為傳入參數的方法,在JS中多到一個程度,語法也是很常見,可見在真實的應用情況,如果能像箭頭函式能作週邊的捕抓this值,作為自己的預設this,可以得到多大的方便與減少多少的濳在問題。
當然,箭頭函式的Lexical this
(詞法的this)作用在大部的情況都可以運作得很好,但它也有在某些下不適合使用的情況,因為Lexical this
(詞法的this)一旦綁定過了,就無法再覆蓋,即使是用new關鍵字也不行。不過,這部份可能會比較進階,你可以參考You Don't Know JS: this & Object Prototypes與這篇文章中的內容。也因為如此,有些情況下你不應該使用箭頭函式,在下面的內容中會說明。
總結來說,這一章的關於this在原本(傳統)的函式與箭頭函式是不同之處如下:
傳統函式: this值是動態的,由呼叫這個函式的擁有者物件(Owner)決定
箭頭函式: this是lexical(詞法的)作用域決定,也就是由週邊的作用域所決定
不可使用箭頭函式的情況
以下這幾個範例都是與this
值有關,所以說如果你的箭頭函式裡有用到this
值的確要特別注意,並不是不能用而是要小心它的行為是不是你要的結果。
以下的每個範例都只能用一般的函式定義方式,"不可"使用箭頭函式。
註: 本節的內容大部份是參考自When 'not' to use arrow functions
用物件字面文字定義物件時,物件中的方法
因為箭頭函式會以物件在定義時的捕捉到的週邊this
為預設this
,也就是window或全域物件(或是在嚴格模式的undefined)。所以會造成是存取不到物件中的array
屬性值。
在物件的prototype屬性中定義的方法
這種情況與上面一點類似,箭頭函式的this
值這時會是window或全域物件(或是在嚴格模式的undefined)。
DOM事件處理的監聽者(事件處理函式)
箭頭函式的this
值,相當於window或全域物件(或是在嚴格模式的undefined)。這裡的this
值如果用一般函式定義的寫法,正確應該是要對應到被監聽DOM元素本身。
建構函式
箭頭函式沒有constructor這個設計(原本的函式中有),直接使用new
運算符時會拋出例外產生錯誤。
其他注意的限制或陷阱
函式物件中的
call
、apply
、bind
三個方法,無法"覆蓋"箭頭函式中的this
值。箭頭函式沒有原本(傳統)的函式有的隱藏arguments物件。
箭頭函式不能當作generators使用,使用
yield
會產生錯誤。在一些函式庫像jQuery、underscore函式庫有些有使用callback(回調, 回呼)的API中不一定可以用。
撰寫風格建議
callback(回調, 回呼)優先使用箭頭函式。 (Airbnb 8.1, Google 5.5.3)
雖然箭頭函式的左邊(傳入參數)只有一個時可以省略圓括號(
()
),但建議你還是不論幾個都用圓括號框起來。(Google 5.5.3, eslint: arrow-parens)避免合併使用箭頭函式與其他的比較運算符(>=, <=),這會造成閱讀上不使與混亂。(Airbnb 8.5)
肥箭頭符號的前後要加一個空格,不要黏在一起。另外,不要直接在符號前後換行。(前面不行,後面要用圓括號或花括號,上面有說明) (eslint: arrow-spacing)
結論
本章對於箭頭函式的用法作了一翻全面性的說明,語法上很簡單,但其中比較不同的是對箭頭函式中的Lexical this
(詞法的this)作了一些簡單的說明,相信你看過之後,應該會對箭頭函式有更進一步的認識。這個ES6新的函式語法,改善了許多長久以來在函式中使用上的this綁定問題。在之後的類別章節中,我們會再見到箭頭函式的用法。
Last updated