箭頭函數( Arrow Function )很早就被應用在 JavaScript 中。第一版 JavaScript 教程中建議包裝腳本并內嵌在 HTML 解釋中。這將會阻止瀏覽器不支持 JS 時,以文本形式錯誤地顯示你的 JS 代碼。你最好這樣寫:
<script language="javascript">
<!--
document.bgColor = "brown"; // red
// -->
</script>
老的瀏覽器會認為上面的代碼是兩種不支持的標簽和解釋;只有新的瀏覽器會認為是 JS 代碼。
在你的瀏覽器中 JavaScript 引擎認為字符 <!-- 作為注釋的開始。不開玩笑地說,這真的是語言的一部分,并且直到現在,不僅僅是內聯(lián)的頂部 <script> ,整個 JS 代碼任意部分都屬于語言的一部分。它甚至可以在節(jié)點處被執(zhí)行。
湊巧的是,這種注釋的風格是 ES6 首次標準化。但這不是我們談論的箭頭函數。
箭頭序列 --> 表示單行注釋。奇怪地是,在 HTML 中在字符串 --> 前面是注釋部分,而在 JS 中字符串 --> 后面的部分表示注釋。
令人感到奇怪的是,這個箭頭只有當它出現在一行的開始才表示注釋。這是因為在內容的其它部分, --> 表示 JS 操作符, 表示 ”跳轉“操作符。
function countdown(n) {
while (n --> 0) // "n goes to zero"
alert(n);
blastoff();
}
這段代碼真的會工作。循環(huán)運行直到 n 變?yōu)?0 。 這也不是 ES6 的新特性,但是一個常見特征的組合。你能想象發(fā)生了什么嗎?像往常一樣,這個謎題的答案可以找到堆棧溢出。
當然,這里也有小于或等于操作符 <=。 或許你還可以在 JS代碼中發(fā)現更多的箭頭函數。讓我們停下來觀察這些箭頭函數,發(fā)現有個箭頭函數消失了。
| 箭頭 | 說明 |
|---|---|
| <!-- | 單行注釋 |
| --> | ”跳轉”操作 |
| <= | 小于或等于操作符 |
| => | ??? |
=>發(fā)生了什么呢?今天,讓我們來尋找它。
首先,讓我們來討論一些功能。
JavaScript 的一個有趣特性是,任何時候你需要一個函數,你可以在函數右邊加入運行代碼。
舉個例子,假設你試圖告訴瀏覽器當用戶點擊一個特殊按鈕后怎么處理。你可以輸入這樣的代碼:
$("#confetti-btn").click(
jQuery’s .click( ) 方法需要一個參數:一個函數。毫無問題,你可以在函數右邊這樣輸入:
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
像上面方式來寫代碼對于我們來說很自然。所以很好奇在 JavaScript 流行以前,很多語言都還不具備這種編程風格。當然 在 1958 年時,Lisp 語言已經有了函數表達式,通常也被稱為 lambda 函數。 但是像 C++, Python, C# 和Java 存在很多年了都還沒有具備這種編程風格。
至少現在上述四種語言已經具備了 lambda 函數。較新的語言通常都已經內置有l(wèi)ambda 函數了。我們能有現在的 JavaScript 應該感謝早起的 JavaScript 開發(fā)者,他們創(chuàng)建了依賴于 lambdas 函數的函數庫,導致這一特征被廣泛采用。
這里有點略帶憂傷,在我提及的所有語言中, JavaScript 關于 lambdas 函數的介紹稍微有點冗長。
// A very simple function in six languages.
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 提供了新的語法規(guī)則來描述函數。
// ES5
var selected = allJobs.filter(function (job) {
return job.isSelected();
});
// ES6
var selected = allJobs.filter(job => job.isSelected());
當你僅僅只需要帶有一個參數的函數,這個新的箭頭函數語法簡單地描述為:標識符 => 表達式 ( Identifier => Expression ) 。你可以跳過編輯函數名和返回值,以及一些大括號,小括號和分號。
( 我個人非常感激這個特征。對于我來說,不用再次編輯函數很重要,因為我不可避免會輸錯函數名,這樣我不得不再回頭檢查并且改正。 )
在編寫一個帶有多個參數的函數時(或許沒有參數,或許有默認參數,或者是前面提到的非結構化參數),你需要在參數列表的外面加一層括號。
// ES5
var total = values.reduce(function (a, b) {
return a + b;
}, 0);
// ES6
var total = values.reduce((a, b) => a + b, 0);
我認為這樣的書寫那個時候看起來很美觀。
箭頭函數就像是在函數庫上編寫一樣, 顯得美觀。實際上,Immutable’s documentation 很多例子都是用 ES6 來編寫的,因此里面的很多例子都使用到了箭頭函數。
箭頭函數內除了可以有表達式外,還可以有一段語句。回想早期的例子:
// ES5
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
這里是用 ES6 寫的代碼:
// ES6
$("#confetti-btn").click(event => {
playTrumpet();
fireConfettiCannon();
});
注意到一個箭頭函數,用塊來描述的時候可能沒有自動返回一個值。我們可以用 return 聲明來返回。
這里有個注意事項當用箭頭函數來創(chuàng)建一個空對象時,要用括號括起來:
// create a new empty object for each puppy to play with
var chewToys = puppies.map(puppy => {}); // BUG!
var chewToys = puppies.map(puppy => ({})); // ok
不幸的是,一個空的對象 {} 和一個空塊 {} 看起來是一樣的。在 ES6 當中 { 緊跟在箭頭函數后面表示的是一個塊的開始,而不是一個對象的開始。這樣代碼 puppy => {} 表示的是一個箭頭函數,什么都不做,空操作。返回一個未定義。
更令人困惑的是,像 {key: value} 這樣的對象聲明看起來像是一個包含有標簽的塊聲明。至少對你的 JavaScript 引擎來說是這樣的。幸運地是,你可以用括號將 { 這個歧義字符括起來。
普通的函數和箭頭函數還是存在細微的差別。箭頭函數沒有自己內部的 this 指針。在箭頭函數中, this 指針是繼承于其所在的作用域。
讓我們在實際使用的過程中來搞明白這到底是什么意思。
在JavaScript 中 this 指針是怎么工作的?它的值又是來自哪里?這里沒有簡潔的答案。如果你認為這是很簡單的,那是因為你對于 this 指針已經很熟悉了。
這一問題經常出現的一個原因是 function 函數自動獲得 this 指針,不管你想要與否。你曾經寫過這樣的代碼嗎?
{
...
addAll: function addAll(pieces) {
var self = this;
_.each(pieces, function (piece) {
self.add(piece);
});
},
...
}
這里你所寫的內部函數是 this.add(piece)。不幸地是,這個內部函數沒有繼承外部函數的 this 指針。這樣內部的函數用 this 將會返回未定義。這個臨時的變量 self 會調用外部的 this 到內部的函數來。( 另外一種方式在內部函數中調用 .bind(this) 。其他方式都不是很令人滿意。 )
在 ES6 中, this 指針的 hacks 代碼可以避免如果你遵循下面的規(guī)則:
// ES6
{
...
addAll: function addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
ES6 版本中,注意到 addAll 方法從調用者那里獲得一個 this 指針。上面內部的函數是一個箭頭函數,它從作用域處繼承 this 指針。
作為獎勵, ES6 通常提供了一個更短的方式來描述對象! 因此上面的代碼可以變得更簡潔:
// ES6 with method syntax
{
...
addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
比較上面的兩種方式,可以發(fā)現用箭頭函數描述可以不再輸入 function 了。這是一個很好的想法。
普通函數和箭頭函數還有一個小差異,那就是箭頭函數不需要參數。當然, 在 ES6 中,你可能寧愿用使用其他參數或者默認參數。
我們這里討論一些關于箭頭函數的實際應用。這里我會討論一個用例: ES6 箭頭函數作為一個學習工具,來發(fā)現一些深層次的計算本質。不管實際與否,你可以自己來判斷。
早在 1936 年,Alan Church 和 Alan Turing 獨立開發(fā)了具有強大計算功能的數學模型。Turing 稱它的模型為一個機器,即其余人所稱道的圖靈機。Church 的模型被他稱為 λ- 演算。( λ 是一個希臘小寫字母 lambda 。 )這也是為什么 Lisp 語言也用 LAMBDA 單詞來表示函數的原因,這也是今天我們稱函數表達式為 “l(fā)ambdas” 的原因。
但是什么又是 λ- 演算呢?什么又是計算模型的思想呢?
這里很難用幾句話來解釋,但是這里是我的一些嘗試性解釋: λ- 演算是最早的編程語言。它的目的不是為了設計編程語言——在此之后,直到十年或到二十年后的樣子才有了存儲程序計算機,但是相當簡單,它只能執(zhí)行一些你想要的純粹數學表達式。 Church 想讓他的模型能夠證明一些概括性的計算。
并且他只需要一樣東西在他的系統(tǒng)里就是:函數。
他的想法是多么的奇怪。沒有對象,沒有數組,沒有數字,還沒有 if 聲明, while 循環(huán) , 分號和賦值,還沒有邏輯操作符,或者是事件,這對于 JavaScript 來說是可能的,只用函數來重新構建每種類型。
這里給出一個數學上的程序,用 Church’s 的 λ 表示法:
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 包含了一個實現 λ- 演算的部分。 即 λ- 演算在JavaScript 中。
關于 Alonzo Church 和后續(xù)的研究者關于 λ- 演算做的研究,以及它揭示了 λ- 演算如何成為大多數語言中的一部分,這些內容超出了這一章節(jié)的范圍。但是如果你對計算機科學的起源很感興趣,或者你對一個語言最開始只有函數沒有任何其他類型感興趣,你可以花費你雨天的中午來閱讀 Church numerals 和 fixed-point combinator,并且你還可以在你的 Firefox 控制臺上寫。由于 ES6 在箭頭函數方面的優(yōu)勢, JavaScript 可以合理地聲明自己的語言對于探索 λ- 演算是最好的語言。
ES6 的箭頭函數是由我于 2013 年在 Firefox 中實現。 Jan de Mooij 使得箭頭函數運行變得更快。感謝 Tooru Fujisawa 和 ziyunfei 提供補丁。
箭頭函數也在微軟的新版本中實現。他們也在 Babel,Traceur,和 TypeScript 得到實現,如果你對于在網上使用箭頭函數很感興趣。
在下一章節(jié)中我們又會討論 ES6 的一個奇怪用法。我們將看到 typeof x 會返回一個全新的值。我們將會發(fā)問:到底什么時候是一個名稱什么時候又是一個字符串呢?我們會努力思考這些問題。這些都令人感到新奇。所以請下周繼續(xù)加入我們來深度探索 ES6 關于符號的新特征。