深入解析 ES6:Generator

發表於2015-07-25

今天討論的新特性讓我非常興奮,因為這個特性是 ES6 中最神奇的特性。

這裡的“神奇”意味著什麼呢?對於初學者來說,該特性與以往的 JS 完全不同,甚至有些晦澀難懂。從某種意義上說,它完全改變了這門語言的通常行為,這不是“神奇”是什麼呢。

不僅如此,該特性還可以簡化程式程式碼,將複雜的“回撥堆疊”改成直線執行的形式。

我是不是鋪墊的太多了?下面開始深入介紹,你自己去判斷吧。

簡介

什麼是 Generator?

看下面程式碼:

上面程式碼是模仿 Talking cat(當下一個非常流行的應用)的一部分,點選這裡試玩,如果你對程式碼感到困惑,那就回到這裡來看下面的解釋。

這看上去很像一個函式,這被稱為 Generator 函式,它與我們常見的函式有很多共同點,但還可以看到下面兩個差異:

  • 通常的函式以 function 開始,但 Generator 函式以 function* 開始。
  • 在 Generator 函式內部,yield 是一個關鍵字,和 return 有點像。不同點在於,所有函式(包括 Generator 函式)都只能返回一次,而在 Generator 函式中可以 yield 任意次。yield 表示式暫停了 Generator 函式的執行,然後可以從暫停的地方恢復執行。

常見的函式不能暫停執行,而 Generator 函式可以,這就是這兩者最大的區別。

原理

呼叫 quips() 時發生了什麼?

我們對普通函式的行為非常熟悉,函式被呼叫時就立即執行,直到函式返回或丟擲一個異常,這是所有 JS 程式設計師的第二天性。

Generator 函式的呼叫方法與普通函式一樣:quips("jorendorff"),但呼叫一個 Generator 函式時並沒有立即執行,而是返回了一個 Generator 物件(上面程式碼中的 iter),這時函式就立即暫停在函式程式碼的第一行。

每次呼叫 Generator 物件的 .next() 方法時,函式就開始執行,直到遇到下一個 yield 表示式為止。

這就是為什麼我們每次呼叫 iter.next() 時都會得到一個不同的字串,這些都是在函式內部通過 yield 表示式產生的值。

當執行最後一個 iter.next() 時,就到達了 Generator 函式的末尾,所以返回結果的 .done屬性值為 true,並且 .value 屬性值為 undefined

現在,回到 Talking cat 的 DEMO,嘗試在程式碼中新增一些 yield 表示式,看看會發生什麼。

從技術層面上講,每當 Generator 函式執行遇到 yield 表示式時,函式的棧幀 — 本地變數,函式引數,臨時值和當前執行的位置,就從堆疊移除,但是 Generator 物件保留了對該棧幀的引用,所以下次呼叫 .next() 方法時,就可以恢復並繼續執行。

值得提醒的是 Generator 並不是多執行緒。在支援多執行緒的語言中,同一時間可以執行多段程式碼,並伴隨著執行資源的競爭,執行結果的不確定性和較好的效能。而 Generator 函式並不是這樣,當一個 Generator 函式執行時,它與其呼叫者都在同一執行緒中執行,每次執行順序都是確定的,有序的,並且執行順序不會發生改變。與執行緒不同,Generator 函式可以在內部的 yield 的標誌點暫停執行。

通過介紹 Generator 函式的暫停、執行和恢復執行,我們知道了什麼是 Generator 函式,那麼現在丟擲一個問題:Generator 函式到底有什麼用呢?

迭代器

通過上篇文章,我們知道迭代器並不是 ES6 的一個內建的類,而只是作為語言的一個擴充套件點,你可以通過實現 [Symbol.iterator]() 和 .next() 方法來定義一個迭代器。

但是,實現一個介面還是需要寫一些程式碼的,下面我們來看看在實際中如何實現一個迭代器,以實現一個 range 迭代器為例,該迭代器只是簡單地從一個數累加到另一個數,有點像 C 語言中的 for (;;) 迴圈。

現在有一個解決方案,就是使用 ES6 的類。(如果你對 class 語法還不熟悉,不要緊,我會在將來的文章中介紹。)

檢視該 DEMO

這種實現方式與 Java 和 Swift 的實現方式類似,看上去還不錯,但還不能說上面程式碼就完全正確,程式碼沒有任何 Bug?這很難說。我們看不到任何傳統的 for (;;) 迴圈程式碼:迭代器的協議迫使我們將迴圈拆散了。

在這一點上,你也許會對迭代器不那麼熱衷了,它們使用起來很方便,但是實現起來似乎很難。

我們可以引入一種新的實現方式,以使得實現迭代器更加容易。上面介紹的 Generator 可以用在這裡嗎?我們來試試:

檢視該 DEMO

上面這 4 行程式碼就可以完全替代之前的那個 23 行的實現,替換掉整個 RangeIterator 類,這是因為 Generator 天生就是迭代器,所有的 Generator 都原生實現了 .next() 和 [Symbol.iterator]() 方法。你只需要實現其中的迴圈邏輯就夠了。

不使用 Generator 去實現一個迭代器就像被迫寫一個很長很長的郵件一樣,本來簡單的表達出你的意思就可以了,RangeIterator 的實現是冗長和令人費解的,因為它沒有使用迴圈語法去實現一個迴圈功能。使用 Generator 才是我們需要掌握的實現方式。

我們可以使用作為迭代器的 Generator 的哪些功能呢?

  • 使任何物件可遍歷 — 編寫一個 Genetator 函式去遍歷 this,每遍歷到一個值就 yield 一下,然後將該 Generator 函式作為要遍歷的物件上的 [Symbol.iterator] 方法的實現。
  • 簡化返回陣列的函式 — 假如有一個每次呼叫時都返回一個陣列的函式,比如:

使用 Generator 可以簡化這類函式:

這兩者唯一的區別在於,前者在呼叫時計算出了所有結果並用一個陣列返回,後者返回的是一個迭代器,結果是在需要的時候才進行計算,然後一個一個地返回。

  • 無窮大的結果集 — 我們不能構建一個無窮大的陣列,但是我們可以返回一個生成無盡序列的 Generator,並且每個呼叫者都可以從中獲取到任意多個需要的值。
  • 重構複雜的迴圈 — 你是否想將一個複雜冗長的函式重構為兩個簡單的函式?Generator 是你重構工具箱中一把新的瑞士軍刀。對於一個複雜的迴圈,我們可以將生成資料集那部分程式碼重構為一個 Generator 函式,然後用 for-of 遍歷:for (var data of myNewGenerator(args))
  • 構建迭代器的工具 — ES6 並沒有提供一個可擴充套件的庫,來對資料集進行 filter 和 map等操作,但 Generator 可以用幾行程式碼就實現這類功能。

例如,假設你需要在 Nodelist 上實現與 Array.prototype.filter 同樣的功能的方法。小菜一碟的事:

所以,Generator 很實用吧?當然,這是實現自定義迭代器最簡單直接的方式,並且,在 ES6 中,迭代器是資料集和迴圈的新標準。

但,這還不是 Generator 的全部功能。

非同步程式碼

下面是我之前寫過的 JS 程式碼(表示程式碼縮排層次太多):

你也許也寫過這樣的程式碼。非同步 API 通常都需要一個回撥函式,這意味著每次你都需要編寫一個匿名函式來處理非同步結果。如果同時處理三個非同步事務,我們看到的是三個縮排層次的程式碼,而不僅僅是三行程式碼。

看下面程式碼:

非同步 API 通常都有錯誤處理的約定,不同的 API 有不同的約定。大多數情況下,錯誤是預設丟棄的,甚至有些將成功也預設丟棄了。

直到現在,這些問題仍是我們處理非同步程式設計必須付出的代價,而且我們也已經接受了非同步程式碼只是看不來不像同步程式碼那樣簡單和友好。

Generator 給我們帶來了希望,我們可以不再採用上面的方式。

Q.async()是一個將 Generator 和 Promise 結合起來處理非同步程式碼的實驗性嘗試,讓我們的非同步程式碼類似於相應的同步程式碼。

例如:

最大的區別在於,需要在每個非同步方法呼叫的前面新增 yield 關鍵字。

在 Q.async 中,新增一個 if 語句或 try-catch 異常處理,就和在同步程式碼中的方式一樣,與其他編寫非同步程式碼的方式相比,減少了很多學習成本。

如果你想深入閱讀,可以參考 James Long 的文章

Generator 為我們提供了一種更適合人腦思維方式的非同步程式設計模型。但更好的語法也許更有幫助,在 ES7 中,一個基於 Promise 和 Generator 的非同步處理函式正在規劃之中,靈感來自 C# 中類似的特性。

相容性

在伺服器端,現在就可以直接在 io.js 中使用 Generator(或者在 NodeJs 中以 --harmony 啟動引數來啟動 Node)。

在瀏覽器端,目前只有 Firefox 27 和 Chrome 39 以上的版本才支援 Generator,如果想直接在 Web 上使用,你可以使用 Babel 或 Google 的 Traceur 將 ES6 程式碼轉換為 Web 友好的 ES5 程式碼。

一些題外話:JS 版本的 Generator 最早是由 Brendan Eich 實現,他借鑑了 Python Generator的實現,該實現的靈感來自 Icon,早在 2006 年的 Firefox 2.0 就吸納了 Generator。但標準化的道路是坎坷的,一路下來,其語法和行為都發生了很多改變,Firefox 和 Chrome 中的 ES6 Generator 是由 Andy Wingo 實現 ,這項工作是由 Bloomberg 贊助的。

yield;

關於 Generator 還有一些未提及的部分,我們還沒有涉及到 .throw() 和 .return() 方法的使用,.next() 方法的可選引數,還有 yield* 語法。但我認為這篇文章已經夠長了,就像 Generator 一樣,我們也暫停一下,另外找個時間再剩餘的部分。

我們已經介紹了 ES6 中兩個非常重要的特性,那麼現在可以大膽地說,ES6 將改變我們的生活,看似簡單的特性,卻有極大的用處。

接下來將介紹一個你每天寫的程式碼都將接觸到的特性 — template string。

相關文章