深入淺出ES6:箭頭函數

箭頭符號在JavaScript誕生時就已經存在,當初第一個JavaScript教程曾建議在HTML注釋內包裹行內腳本,這樣可以避免不支持JS的瀏覽器誤將JS代碼顯示為文本。你會寫這樣的代碼:

<scriptlanguage="javascript">

<!--

? ?document.bgColor = "brown";? // red

? ?// -->

</script>

老式瀏覽器會將這段代碼解析為兩個不支持的標簽和一條注釋,只有新式瀏覽器才能識別出其中的JS代碼。

為了支持這種奇怪的hack方式,瀏覽器中的JavaScript引擎將標簽后的首行,在JS代碼的每個角落你都有可能見到它,甚至在Node中也是如此。-->

碰巧,這種注釋風格首次在ES6中被標準化了,但在新標準中箭頭被用來做其它事情。

箭頭序列-->同樣是單行注釋的一部分。古怪的是,在HTML中-->之前的字符是注釋的一部分,而在JS中-->之后的部分才是注釋。

你一定感到陌生的是,只有當箭頭在行首時才會注釋當前行。這是因為在其它上下文中,-->是一個JS運算符:“趨向于”運算符!

function countdown(n) {

while(n-->0) ? ? // "n goes to zero"

alert(n);

blastoff();

}

上面這段代碼可以正常運行,循環會一直重復直到n趨于0,這當然不是ES6中的新特性,它只不過是將兩個你早已熟悉的特性通過一些誤導性的手段結合在一起。你能理解么?通常來說,類似這種謎團都可以在Stack Overflow上找到答案。

當然,同樣地,小于等于操作符<=也形似箭頭,你可以在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){returna>0;}// JS

[](int a){returna>0;}// C++

(lambda(a)(>a0));;Lisp

lambda a:a>0# Python

a=>a>0// C#

a->a>0// Java

箭袋中的新羽

ES6中引入了一種編寫函數的新語法

// ES5

var selected=allJobs.filter(function(job){

returnjob.isSelected();

});

// ES6

var selected = allJobs.filter(job=>job.isSelected());

當你只需要一個只有一個參數的簡單函數時,可以使用新標準中的箭頭函數,它的語法非常簡單:標識符=>表達式。你無需輸入function和return,一些小括號、大括號以及分號也可以省略。

(我個人對于這個特性非常感激,不再需要輸入function這幾個字符對我而言至關重要,因為我總是不可避免地錯誤寫成functoin,然后我就不得不回過頭改正它。)

如果要寫一個接受多重參數(也可能沒有參數,或者是不定參數、默認參數、參數解構)的函數,你需要用小括號包裹參數list。

// ES5

artotal=values.reduce(function(a,b){

returna+b;

},0);

// ES6

var total=values.reduce((a,b)=>a+b,0);

我認為這看起來酷斃了。

正如你使用類似Underscore.js和Immutable.js這樣的庫提供的函數工具,箭頭函數運行起來同樣美不可言。事實上,Immutable的文檔中的示例全都由ES6寫成,其中的許多特性已經用上了箭頭函數。

那么不是非常函數化的情況又如何呢?除表達式外,箭頭函數還可以包含一個塊語句。回想一下我們之前的示例:

// ES5

$("#confetti-btn").click(function(event){

playTrumpet();

fireConfettiCannon();

});

這是它們在ES6中看起來的樣子:

// ES6

$("#confetti-btn").click(event=>{

playTrumpet();

fireConfettiCannon();

});

這是一個微小的改進,對于使用了Promises的代碼來說箭頭函數的效果可以變得更加戲劇性,}).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是如何工作的?它的值從哪里獲???這些問題的答案可都不簡單,如果你對此倍感清晰,一定因為你長時間以來一直在處理類似的問題。

這個問題經常出現的其中一個原因是,無論是否需要,function函數總會自動接收一個this值。你是否寫過這樣的hack代碼:

{

...

addAll:functionaddAll(pieces){

varself=this;

_.each(pieces,function(piece){

self.add(piece);

});

},

...

}

在這里,你希望在內層函數里寫的是this.add(piece),不幸的是,內層函數并未從外層函數繼承this的值。在內層函數里,this會是window或undefined,臨時變量self用來將外部的this值導入內部函數。(另一種方式是在內部函數上執行.bind(this),兩種方法都不甚美觀。)

在ES6中,不需要再hackthis了,但你需要遵循以下規則:

通過object.method()語法調用的方法使用非箭頭函數定義,這些函數需要從調用者的作用域中獲取一個有意義的this值。

其它情況全都使用箭頭函數。

// ES6

{

...

addAll:functionaddAll(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演算(λ-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函數是這樣的:

varfix=f=>(x=>f(v=>x(x)(v)))

? ? ? ? ? ? ? ? (x=>f(v=>x(x)(v)));

所以,在JavaScript中實現了一個可以運行的lambda演算,它根植于這門語言中。

Alonzo Church和lambda演算后繼研究者們的故事,以及它是如何潛移默化地入駐每一門主流編程語言的,已經遠超本文的討論范圍。但是如果你對計算機科學 的奠基感興趣,或者你只是對一門只用函數就可以做許多類似循環和遞歸這樣的事情的語言倍感興趣,你可以在一個下雨的午后深入邱奇數(Church numerals)和不動點組合子(Fixed-point combinator),在你的Firefox控制臺或Scratchpad中仔細研究一番。結合ES6的箭頭函數以及其它強大的功能,JavaScript稱得上是一門探索lambda演算的最好的語言。

我何時可以使用箭頭函數?

早在2013年,我就在Firefox中實現了ES6箭頭函數的功能,Jan de Mooij為其優化加快了執行速度。感謝Tooru Fujisawa以及ziyunfei(譯者注:中國開發者,為Mozilla作了許多貢獻)后續打的補丁。

微軟Edge預覽版中也實現了箭頭函數的功能,如果你想立即在你的Web項目中使用箭頭函數,可以使用Babel、Traceur或TypeScript,這三個工具均已實現相關功能。


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1.函數參數的默認值 (1).基本用法 在ES6之前,不能直接為函數的參數指定默認值,只能采用變通的方法。
    趙然228閱讀 707評論 0 0
  • 原文鏈接:https://github.com/EasyKotlin 值就是函數,函數就是值。所有函數都消費函數,...
    JackChen1024閱讀 6,067評論 1 17
  • 函數參數的默認值 基本用法 在ES6之前,不能直接為函數的參數指定默認值,只能采用變通的方法。 上面代碼檢查函數l...
    呼呼哥閱讀 3,476評論 0 1
  • 本文由我們團隊的 糾結倫 童鞋撰寫。 寫在前面 本篇文章是對我一次組內分享的整理,大部分圖片都是直接從keynot...
    知識小集閱讀 15,291評論 11 172
  • 寫在前面 本篇文章是對我一次組內分享的整理,大部分圖片都是直接從keynote上截圖下來的,本來有很多炫酷動效的,...
    等開會閱讀 14,535評論 6 69