深入淺出ES6(七):箭頭函數 Arrow Functions


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

    <script language="javascript">
    <!--
      document.bgColor = "brown";  // red
    // -->
    </script>

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

爲了支持這種奇怪的hack方式,瀏覽器中的JavaScript引擎將<!--這四個字符解析爲單行註釋的起始部分,我沒開玩笑,這自始至終就是語言的一部分,直到現在仍然有效,這種註釋符號不僅出現<script>標籤後的首行,在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) { 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());

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

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

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

    // ES5
    var total = values.reduce(function (a, b) {
      return a + b;
    }, 0);
    // ES6
    var total = values.reduce((a, b) => a + b, 0);

我認爲這看起來酷斃了。

正如你使用類似Underscore.jsImmutable.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: function addAll(pieces) {
        var self = this;
        _.each(pieces, function (piece) {
          self.add(piece);
        });
      },
      ...
    }

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

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

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

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

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

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

發佈了99 篇原創文章 · 獲贊 56 · 訪問量 84萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章