> 原文出處:http://www.infoq.com/cn/articles/es6-in-depth-arrow-functions
[TOC]
箭頭符號在JavaScript誕生時就已經存在,當初第一個JavaScript教程曾建議在HTML注釋內包裹行內腳本,這樣可以避免不支持JS的瀏覽器誤將JS代碼顯示為文本。你會寫這樣的代碼:
~~~
<script language="javascript">
<!--
document.bgColor = "brown"; // red
// -->
</script>
~~~
老式瀏覽器會將這段代碼解析為兩個不支持的標簽和一條注釋,只有新式瀏覽器才能識別出其中的JS代碼。
為了支持這種奇怪的hack方式,瀏覽器中的JavaScript引擎將`<!--`這四個字符解析為單行注釋的起始部分,我沒開玩笑,這自始至終就是語言的一部分,直到現在仍然有效,這種注釋符號不僅出現`<script>`標簽后的首行,在JS代碼的每個角落你都有可能見到它,甚至在Node中也是如此。
碰巧,[這種注釋風格](http://people.mozilla.org/~jorendorff/es6-draft.html#sec-html-like-comments)首次在ES6中被標準化了,但在新標準中箭頭被用來做其它事情。
箭頭序列`-->`同樣是單行注釋的一部分。古怪的是,在HTML中`-->`之前的字符是注釋的一部分,而在JS中`-->`之后的部分才是注釋。
你一定感到陌生的是,只有當箭頭在行首時才會注釋當前行。這是因為在其它上下文中,`-->`是一個JS運算符:“趨向于”運算符!
~~~
function countdown(n) {
while (n --> 0) // "n goes to zero"
alert(n);
blastoff();
}
~~~
上面這段代碼[可以正常運行](http://codepen.io/anon/pen/oXZaBY?editors=001),循環會一直重復直到`n`趨于0,這當然不是ES6中的新特性,它只不過是將兩個你早已熟悉的特性通過一些誤導性的手段結合在一起。你能理解么?通常來說,類似這種謎團都可以在[Stack Overflow](http://stackoverflow.com/questions/1642028/what-is-the-name-of-the-operator)上找到答案。
當然,同樣地,小于等于操作符`<=`也形似箭頭,你可以在JS代碼、隱藏的圖片樣式中找到更多類似的箭頭,但是我們就不繼續尋找了,你應該注意到我們漏掉了一種特殊的箭頭。
|箭頭標記|作用描述|
|---|---|
| `<!--` | `單行注釋` |
| `-->` | `“趨向于”操作符` |
| `<=` | `小于等于` |
| `=>` | `這又是什么?` |
`=>`到底是什么?我們今天就來一探究竟。
首先,我們談論一些有關函數的事情。
## 函數表達式無處不在
JavaScript中有一個有趣的特性,無論何時,當你需要一個函數時,你都可以在想添加的地方輸入這個函數。
舉個例子,假設你嘗試告訴瀏覽器用戶點擊一個特定按鈕后的行為,你會這樣寫:
~~~
$("#confetti-btn").click(
~~~
jQuery的`.click()`方法接受一個參數:一個函數。沒問題,你可以在這里輸入一個函數:
~~~
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
~~~
對 于現在的我們來說,寫出這樣的代碼相當自然,而回憶起在這種編程方式流行之前,這種寫法相對陌生一些,許多語言中都沒有這種特性。1958年,Lisp首 先支持函數表達式,也支持調用lambda函數,而C++,Python、C#以及Java在隨后的多年中一直不支持這樣的特性。
現在截然不同,所有的四種語言都已支持lambda函數,更新出現的語言普遍都支持內建的lambda函數。我們必須要感謝JavaScript和早期的JavaScript程序員,他們勇敢地構建了重度依賴lambda函數的庫,讓這種特性被廣泛接受。
令人傷感的是,隨后在所有我提及的語言中,只有JavaScript的lambda的語法最終變得冗長乏味。
~~~
// 六種語言中的簡單函數示例
function (a) { return a > 0; } // JS
[](int a) { return a > 0; } // C++
(lambda (a) (> a 0)) ;; Lisp
lambda a: a > 0 # Python
a => a > 0 // C#
a -> a > 0 // Java
~~~
## 箭袋中的新羽
ES6中引入了一種編寫函數的新語法
~~~
// ES5
var selected = allJobs.filter(function (job) {
return job.isSelected();
});
// ES6
var selected = allJobs.filter(job => job.isSelected());
~~~
當你只需要一個只有一個參數的簡單函數時,可以使用新標準中的箭頭函數,它的語法非常簡單:`標識符=>表達式`。你無需輸入`function`和`return`,一些小括號、大括號以及分號也可以省略。
(我個人對于這個特性非常感激,不再需要輸入`function`這幾個字符對我而言至關重要,因為我總是不可避免地錯誤寫成`functoin`,然后我就不得不回過頭改正它。)
如果要寫一個接受多重參數(也可能沒有參數,或者是[不定參數、默認參數](http://www.infoq.com/cn/articles/es6-in-depth-rest-parameters-and-defaults)、[參數解構](http://www.infoq.com/cn/articles/es6-in-depth-destructuring))的函數,你需要用小括號包裹參數list。
~~~
// ES5
var total = values.reduce(function (a, b) {
return a + b;
}, 0);
// ES6
var total = values.reduce((a, b) => a + b, 0);
~~~
我認為這看起來酷斃了。
正如你使用類似[Underscore.js](http://underscorejs.org/)和[Immutable.js](https://facebook.github.io/immutable-js/)這樣的庫提供的函數工具,箭頭函數運行起來同樣美不可言。事實上,[Immutable的文檔](https://facebook.github.io/immutable-js/docs/#/)中的示例全都由ES6寫成,其中的許多特性已經用上了箭頭函數。
那么不是非常函數化的情況又如何呢?除表達式外,箭頭函數還可以包含一個塊語句。回想一下我們之前的示例:
~~~
// ES5
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
~~~
這是它們在ES6中看起來的樣子:
~~~
// ES6
$("#confetti-btn").click(event => {
playTrumpet();
fireConfettiCannon();
});
~~~
這是一個微小的改進,對于使用了[Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)的代碼來說箭頭函數的效果可以變得更加戲劇性,`}).then(function (result) {`?這樣的一行代碼可以堆積起來。
注意,使用了塊語句的箭頭函數不會自動返回值,你需要使用`return`語句將所需值返回。
小提示:當使用箭頭函數創建普通對象時,你總是需要將對象包裹在小括號里。
~~~
// 為與你玩耍的每一個小狗創建一個新的空對象
var chewToys = puppies.map(puppy => {}); // 這樣寫會報Bug!
var chewToys = puppies.map(puppy => ({})); //
~~~
用小括號包裹空對象就可以了。
不幸的是,一個空對象`{}`和一個空的塊`{}`看起來完全一樣。ES6中的規則是,緊隨箭頭的{被解析為塊的開始,而不是對象的開始。因此,`puppy => {}`這段代碼就被解析為沒有任何行為并返回`undefined`的箭頭函數。
更令人困惑的是,你的JavaScript引擎會將類似`{key: value}`的對象字面量解析為一個包含標記語句的塊。幸運的是,`{`是唯一一個有歧義的字符,所以用小括號包裹對象字面量是唯一一個你需要牢記的小竅門。
## 這個函數的this值是什么呢?
普通`function`函數和箭頭函數的行為有一個微妙的區別,**箭頭函數沒有它自己的`this`值**,箭頭函數內的`this`值繼承自外圍作用域。
在我們嘗試說明這個問題前,先一起回顧一下。
JavaScript中的`this`是如何工作的?它的值從哪里獲取?[這些問題的答案可都不簡單](http://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work),如果你對此倍感清晰,一定因為你長時間以來一直在處理類似的問題。
這個問題經常出現的其中一個原因是,無論是否需要,`function`函數總會自動接收一個`this`值。你是否寫過這樣的hack代碼:
~~~
{
...
addAll: function addAll(pieces) {
var self = this;
_.each(pieces, function (piece) {
self.add(piece);
});
},
...
}
~~~
在這里,你希望在內層函數里寫的是`this.add(piece)`,不幸的是,內層函數并未從外層函數繼承`this`的值。在內層函數里,`this`會是`window`或`undefined`,臨時變量`self`用來將外部的`this`值導入內部函數。(另一種方式是在內部函數上執行`.bind(this)`,兩種方法都不甚美觀。)
在ES6中,不需要再hack`this`了,但你需要遵循以下規則:
* 通過`object.method()`語法調用的方法使用非箭頭函數定義,這些函數需要從調用者的作用域中獲取一個有意義的`this`值。
* 其它情況全都使用箭頭函數。
~~~
// ES6
{
...
addAll: function addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
~~~
在ES6的版本中,注意`addAll`方法從它的調用者處獲取了`this`值,內部函數是一個箭頭函數,所以它繼承了外圍作用域的`this`值。
超贊的是,在ES6中你可以用更簡潔的方式編寫對象字面量中的方法,所以上面這段代碼可以簡化成:
~~~
// ES6的方法語法
{
...
addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
~~~
在方法和箭頭函數之間,我再也不會錯寫`functoin`了,這真是一個絕妙的設計思想!
箭頭函數與非箭頭函數間還有一個細微的區別,箭頭函數不會獲取它們自己的`arguments`對象。誠然,在ES6中,你可能更多地會使用不定參數和默認參數值這些新特性。
## 借助箭頭函數洞悉計算機科學的風塵往事
我們已經討論了許多箭頭函數的實際用例,它還有一種可能的使用方法:將ES6箭頭函數作為一個學習工具,來深入挖掘計算的本質,是否實用,終將取決于你自己。
1936年,Alonzo Church和Alan Turing各自開發了強大的計算數學模型,圖靈將他的模型稱為a-machines,但是每一個人都稱其為圖靈機。Church寫的是函數模型,他的模型被稱為[lambda演算](https://zh.wikipedia.org/wiki/%CE%9B%E6%BC%94%E7%AE%97)([λ-calculus](https://en.wikipedia.org/wiki/Lambda_calculus))。這一成果也被Lisp借鑒,用`LAMBDA`來指示函數,這也是為何我們現在將函數表達式稱為lambda函數。
但什么是lambda演算呢?“計算模型”又意味著什么呢?
用 幾句話解釋清楚很難,但是我會努力闡釋:lambda演算是第一代編程語言的一種形式,但畢竟存儲程序計算機在十幾二十年后才誕生,所以它原本不是為編程 語言設計的,而是為了表達任意你想到的計算問題設計的一種極度簡化的純數學思想的語言。Church希望用這個模型來證明普遍意義的計算。
最終他發現,在他的系統中只需要一件東西:函數。
這種聲明方式無與倫比,不借助對象、數組、數字、`if`語句、`while`循環、分號、賦值、邏輯運算符甚或是事件循環,只須使用函數就可以從0開始重建JavaScript能實現的每一種計算。
這是用Church的lambda標記寫出來的數學家風格的“程序”示例:
~~~
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))
~~~
等效的JavaScript函數是這樣的:
~~~
var fix = f => (x => f(v => x(x)(v)))
(x => f(v => x(x)(v)));
~~~
所以,在JavaScript中實現了一個可以運行的lambda演算,它根植于這門語言中。
Alonzo Church和lambda演算后繼研究者們的故事,以及它是如何潛移默化地入駐每一門主流編程語言的,已經遠超本文的討論范圍。但是如果你對計算機科學 的奠基感興趣,或者你只是對一門只用函數就可以做許多類似循環和遞歸這樣的事情的語言倍感興趣,你可以在一個下雨的午后深入[邱奇數](https://zh.wikipedia.org/wiki/%E9%82%B1%E5%A5%87%E6%95%B0)([Church numerals](https://en.wikipedia.org/wiki/Church_encoding))和[不動點組合子](https://zh.wikipedia.org/wiki/%E4%B8%8D%E5%8A%A8%E7%82%B9%E7%BB%84%E5%90%88%E5%AD%90)([Fixed-point combinator](https://en.wikipedia.org/wiki/Fixed-point_combinator#Strict_fixed_point_combinator)),在你的Firefox控制臺或[Scratchpad](https://developer.mozilla.org/en-US/docs/Tools/Scratchpad)中仔細研究一番。結合ES6的箭頭函數以及其它強大的功能,JavaScript稱得上是一門探索lambda演算的最好的語言。
## 我何時可以使用箭頭函數?
早在2013年,我就在Firefox中實現了ES6箭頭函數的功能,Jan de Mooij為其優化加快了執行速度。感謝Tooru Fujisawa以及[ziyunfei](https://developer.mozilla.org/zh-CN/profiles/ziyunfei)(譯者注:中國開發者,為Mozilla作了許多貢獻)后續打的補丁。
微軟Edge預覽版中也實現了箭頭函數的功能,如果你想立即在你的Web項目中使用箭頭函數,可以使用[Babel](http://babeljs.io/)、[Traceur](https://github.com/google/traceur-compiler#what-is-traceur)或[TypeScript](http://www.typescriptlang.org/),這三個工具均已實現相關功能。
我們的下一個話題是ES6中的一個非常陌生的特性,我們將一睹`typeof x`返回的全新的類型值。思考一下:不使用字符串如何命名?敬請期待我們下回分解,觀眾老爺們記得回來!