[譯]深入ES6之箭頭函數
箭頭一族缺少的家庭成員
在JavaScript出現以來,箭頭(Arrow)就一直是其語法的一部分。一般來說,JavaScript教程的第一篇就會講如何在HTML中插入箭頭括號來作為注釋,這會阻止不支持JS的瀏覽器錯誤的將你的JS代碼作為文字展現出來,比如這個:
<script language="javascript">
<!--
document.bgColor = "brown"; // red
// -->
</script>
老舊的瀏覽器只會看到兩個不支持的tag以及一個注釋,而現代瀏覽器則會看到JS代碼。
注:其實這是一種hack寫法,在如今現代瀏覽器都支持JS的情況下已經慢慢不會再出現這種代碼了
為了支持這個這個古怪的hack寫法,瀏覽器的JavaScript引擎把 <!--
當成了一行注釋,這不是開玩笑,這成為了這個語言寫法的一部分,并且直到現在都能正常使用。不只是在HTML的 <script>
中,在JS文件中也是一樣,甚至在Node中它也是行得通的。
很巧合的是,我們在ES6中也首次有了注釋的標準格式但是這不是我們現在想要說的箭頭了,在此不贅述。
在-->之后的也會被當成一個注釋,這點很古怪。你可以嘗試在node里輸入--> console.log(1)
結果發現被作為一個注釋而沒有被執行。
但是更古怪的是,只有在箭頭出現在行首的時候才會被當成注釋,這是因為-->在JS中是一個操作符,是一個趨向操作符!
function countdown(n) {
while (n --> 0) // "n goes to zero"
alert(n);
blastoff();
}
這個代碼可以執行。它會從n一直循環到0,這不是ES6的特性,只是有一點小誤導的簡單特性,你能理解發生了什么嗎?謎題的答案在 這里 可以找到。
那么,本該四人組的箭頭還缺了誰呢?
<!-- 單行注釋
--> 趨向操作符
<= 小于等于
=> ?
=> 是ES6中新的用法,也就是本節要講述的內容</code></pre>
更加簡短的函數聲明
JavaScript的一個很有趣的特性就是在你需要一個函數的時候,你可以把它寫到文件中的任何一個地方。
舉個例子,想象當你想要告訴瀏覽器你點擊了一個特定的按鈕的時候,你只需要打出:
$("#confetti-btn").click(
jQuery的click方法只有一個參數:一個函數,沒有問題,你甚至可以直接這樣寫:
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
在JS開始盛行前,這樣的代碼是令人奇怪的,因為很多語言當時都沒有類似的寫法。除了1958年List語言的lambda(匿名)函數有函數表達式功能,C++、Python、C#等語言長時間里都是沒有的。到了現在,lambda已經隨處可見了,這多虧了JS的功勞。
//六種語言的函數
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());</code></pre>
當你只是需要一個簡單的單變量函數,箭頭函數寫起來就非常簡單 Identifier => Expression 你可以省去了寫function和return了,當然,還有中括號,小括號和分號。
(個人來說,我真的很喜歡這個語法,因為我現在不需要再每次都書寫function了,我經常錯寫成functoin,而且每次都要回來檢查和改正它)
如果你想要寫一個多參數的函數(或者沒有參數亦或是Rest參數,甚至是一個解構的變量),都是非常簡單的,你可以直接把他們用括號包起來。
// ES5
var total = values.reduce(function (a, b) {
return a + b;
}, 0);
// ES6
var total = values.reduce((a, b) => a + b, 0);</code></pre>
我認為這是最簡潔的書寫格式。
箭頭函數的功用與Underscore.js和immutable等庫的功能類似, immutable 的示例文檔中全部都是使用ES6來編寫的,因此使用了大量的箭頭函數。
除了函數樣式編寫,箭頭函數還可以包含區塊語句而不僅僅是單一表達式。例如:
// ES5
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
Here’s how it will look in ES6:
// ES6
$("#confetti-btn").click(event => {
playTrumpet();
fireConfettiCannon();
});</code></pre>
只有一點點的改善,還不如使用Promise的提升更多。
但是有需要注意的幾點:
1.當箭頭函數包含的是區塊的時候,它就不會自動return我們的結果
2.而且當我們想要直接使用箭頭函數創造空對象,一定要將其包裹在括號里
var chewToys = puppies.map(puppy => {}); // BUG!
var chewToys = puppies.map(puppy => ({})); // ok
因為空對象和空區塊長的一模一樣,ES6在編譯到 {
的時候會直接認為它是一個區塊,而不是一個Object的開始,所以上面出bug的寫法什么都不會做,因為其函數內部只是返回了一堆 undefined
。
雖然我們會有很多疑惑,比如 {key:value} 這種也會看起來像是一個有標簽陳述式的區塊,而不是一個對象,不過幸運的是 {
是唯一的模糊字,所以我們只需要記住我們的對象要用括號包裹住就好。
只能繼承parent的this值
普通函數與箭頭函數有個微小的不同點。箭頭函數沒有自己的this值,其this值是通過繼承其它傳入對象而獲得的。
JS是如何處理this的呢?this的值又是從何而來的呢?這可不是個簡單的問題。如果你覺得這個已經很簡單了,那么你真的是一名大牛了。
其中不論函數有沒有真的需要處理this,函數都是會接收到的。你寫過如下代碼嗎?
{//ES5
...
addAll: function addAll(pieces) {
var self = this;
_.each(pieces, function (piece) {
self.add(piece);
});
},
...
}
其實你想寫的內聯函數僅僅是this.add(piece)。然而內聯函數不會繼承外部函數的this值。在內聯函數中,this的值是window或undefined。臨時變量self用于向內聯函數傳入外部this值。
在ES6中,如果遵循如下原則則可避免類似的做法:
- 使用非箭頭函數處理由object.method()語法調用的方法。因為它們會接收到來自調用者的有意義的this值.
- 在其它場合都使用箭頭函數.
// ES6
{
...
addAll: function addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
在ES6的版本中,你會注意到addAll函數已經從其調用者獲得了this的值,而且它的內聯函數是一個箭頭函數,所以內斂函數的this繼承了addAll的this。
在Object中其實ES6可以更為精簡的寫方法了,這是因為ES6中的增強字面量(enhanced object literals).
{ //ES6 with method syntax
...
addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
箭頭函數規范
箭頭函數(Arrow Function)并非全能,在一些特別的場景并不適用,比如其沒有函數名字,無法使用遞歸
- 所有箭頭函數的參數均使用()包裹,即便只有一個參數
// Good Style
let foo = (x) => x+1;
// Bad Style
let foo = x => x+1;
- 定義函數盡可能使用箭頭函數
// Good Style
let foo = () => {};
// Bad Style
function foo() {}
// Bad Style
let foo = function () {}</code></pre>
當然如上文的那些特殊情況不適用箭頭函數的則不要使用
-
在對象或者Class中定義函數,使用箭頭函數
這樣能夠最大程度的減少我們寫一些多余的代碼并且增加了代碼的可讀性。
拓展閱讀
- MDN 箭頭函數規范
- 本文由 ES6 In Depth: Arrow functions 翻譯潤色重新組織內容后合成