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