深入淺出 ES6:Generators

今天,我們將要討論的是 ES6 中最奇妙的特性。

為何稱之為"奇妙"呢?對于初學者而言,這個特性與現(xiàn)有 JavaScript 中的內(nèi)容看起來是如此這般的格格不入。從某種角度而言,它將由內(nèi)而外地改變語言的常見行為。如果這還不算奇妙,那還有什么能算呢?

不僅如此,這個特性可以大幅度地簡化現(xiàn)有代碼,并神奇地解決"callback hell" 的問題。

接下來讓我們來深入了解一下這個奇妙的特性吧。

ES6 生成器簡介

什么是生成器(Generator)?

先來看一個例子:

function* quips(name) {
  yield "hello " + name + "!";
  yield "i hope you are enjoying the blog posts";
  if (name.startsWith("X")) {
    yield "it's cool how your name starts with X, " + name;
  }
  yield "see you later!";
}

這是一段會說話的貓中的代碼,也許是當今互聯(lián)網(wǎng)上最重要的一類應用。(點擊鏈接,與這只貓互動。當你感到困惑的時候,再回來看看這篇文章中的解釋。)

生成器(Generator)看起來像是一種函數(shù),對嗎?它確實被稱之為生成器函數(shù),且與函數(shù)有很多相似之處。但是兩者有以下兩點區(qū)別:

  • 普通函數(shù)用 function 聲明。生成器函數(shù)則用 function *
  • 在生成器函數(shù)內(nèi)部,關(guān)鍵詞 yield 是一種類似 return 的語法。區(qū)別就在于函數(shù)(甚至生成器函數(shù))只能返回一次,但是生成器函數(shù)可以 yield 多次。yield 表達式將生成器的執(zhí)行過程掛起,隨后可以被恢復。

所以這是一個普通函數(shù)與生成器函數(shù)之間比較大的區(qū)別。普通函數(shù)無法暫停自身的執(zhí)行,而生成器函數(shù)可以。

生成器是做什么的?

當你調(diào)用 quips() 生成器函數(shù)的時候發(fā)生了什么?

> var iter = quips("jorendorff");
  [object Generator]
> iter.next()
  { value: "hello jorendorff!", done: false }
> iter.next()
  { value: "i hope you are enjoying the blog posts", done: false }
> iter.next()
  { value: "see you later!", done: false }
> iter.next()
  { value: undefined, done: true }

你可能對普通函數(shù)以及它們是如何運行已經(jīng)非常了解。當你調(diào)用它們的時候,就馬上開始運行,直到遇到return語句或者拋出異常。這對于 JavaScript 程序員而言是再熟悉不過的了。

調(diào)用生成器看起來(和普通函數(shù))沒有區(qū)別:quips("jorendorff")。但是當你調(diào)用一個生成器的時候,它還沒有開始運行。反之,它返回了一個暫停的生成器對象(上述例子中的iter)。你可以把生成器對象當做一個函數(shù)調(diào)用,但是立即凍結(jié)了。具體而言,它在運行生成器函數(shù)頂端第一行代碼之前就已經(jīng)凍結(jié)了。

每次調(diào)用生成器對象的.next()方法,函數(shù)就會將自身解凍然后運行直到遇到下一個yield表達式。

這也就是為什么在上面我們每次調(diào)用iter.next()的時候,都會得到一個不同的字符串值。這些值都是由quips()函數(shù)體內(nèi)的yield表達式生成的。

在最后一次調(diào)用iter.next()時,就到了生成器函數(shù)的末尾,所以返回值中的.done字段值為true。函數(shù)執(zhí)行完就如同返回了一個undefined,這也就是為什么返回值中的.value字段的值為undefined

現(xiàn)在可以回過頭來看會說話的貓的 demo。嘗試著把yield放到一個循環(huán)中,會發(fā)生什么?

就技術(shù)角度而言,每當一個生成器執(zhí)行 yield 操作的時候,它的棧結(jié)構(gòu)內(nèi)的本地變量,參數(shù),臨時變量以及生成器內(nèi)部的執(zhí)行位置都會被移出棧。但是生成器對象本身維持了一個對棧結(jié)構(gòu)的引用或者拷貝,所以之后的 .next() 調(diào)用可以重新激活生成器隨后繼續(xù)執(zhí)行。

值得注意的是,生成器不是線程。支持線程的語言中,多段不同的代碼可以在同一時候運行,這經(jīng)常會導致竟態(tài)條件、不確定性以及不錯的性能提升。生成器則完全不同。當生成器運行的時候,它會在叫做 caller 的同一個線程中運行。執(zhí)行的順序是有序、確定的,并且永遠不會產(chǎn)生并發(fā)。不同于系統(tǒng)的線程,生成器只會在其內(nèi)部用到 yield 的時候才會被掛起。

好了。既然已經(jīng)知道生成器是什么,也看到生成器是如何運行、暫停然后恢復執(zhí)行。那么問題來了,這個奇怪的功能到底有什么用處呢?

生成器是迭代器

在上一篇文章中,我們了解到 ES6 迭代器不僅僅是一個單獨的內(nèi)建類。同時也是對語言的一個擴展點。你可以通過實現(xiàn)兩個方法:[Symbol.iterator]().next() 創(chuàng)建自己的迭代器。

但是實現(xiàn)一個接口總歸不是一件小事。讓我們來看看如何在實踐中實現(xiàn)一個迭代器。舉個例子,來創(chuàng)建一個簡單的 range 迭代器 -- 可以從一個數(shù)字計數(shù)到另一個,類似C 語言中經(jīng)典的 for(;;) 循環(huán)。

// 以下代碼將會輸出三次 Ding!
for (var value of range(0, 3)) {
  alert("Ding! at floor #" + value);
}

以下是一個使用 ES6 class 的解決方案。(如果對 class 語法不是完全了解,不要擔心,我們將會在之后的另一篇文章中介紹)

class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

  [Symbol.iterator]() { return this; }

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    } else {
      return {done: true, value: undefined};
    }
  }
}

// 返回一個新的迭代器,從 'start' 計數(shù)到 'stop'.
function range (start, stop) {
  return new RangeIterator(start, stop);
}

查看此段代碼的運行實例

這是像 Java 或 Swift 那樣實現(xiàn)的一個迭代器。不是很糟糕,但是也不是完全沒有問題。那么這段代碼會有 bug 嗎?不好說。它看起來與我們一開始要實現(xiàn)的 for(;;) 循環(huán)沒有絲毫相像之處:迭代器的協(xié)議迫使我們拆解了循環(huán)。

此時你也許已經(jīng)對迭代器不是那么感興趣了。他們也許用起來非常不錯,但是看起來卻難以實現(xiàn)。

也許你不會建議為了讓迭代器的構(gòu)建更為容易而使用我們介紹的這種 JavaScript 語言中新的陌生而復雜的控制流程結(jié)構(gòu)。但是既然我們有了生成器,那么為什么在此去使用它呢?一起來試試吧:

function* range(start, stop) {
  for (var i = start; i < stop; i++)
    yield i;
}

查看此段代碼的運行實例

上述使用 generator 的4行代碼可以替代之前用23行代碼的包含了完整的 RangeIterator 類的range()實現(xiàn)。這大概是因為 generator 也屬于迭代器。所有的生成器都內(nèi)置實現(xiàn)了 .next()[Symbol.iterator]() 方法。你只需編寫循環(huán)部分的邏輯。

不使用生成器實現(xiàn)迭代器就像完全用被動語句寫一篇很長的郵件。如果無法簡練地表達,那么說出來的話可能會相當晦澀難懂。由于 RangeIterator 并沒有用循環(huán)的語法來描述一個循環(huán)的功能,從而令人覺得它額外的冗長、怪異。相反的,生成器才是這個問題的答案。

將其當做迭代器,還能如何發(fā)揮生成器的能力呢?

  • 令任意對象可迭代。只需寫一個生成器函數(shù)遍歷這個對象,在此過程中把每個值yield。然后將這個生成器函數(shù)作為對象的[Symbol.iterator]方法。

  • 簡化數(shù)組構(gòu)建的函數(shù)。假設(shè)你有一個函數(shù),每次調(diào)用的時候都返回一個結(jié)果的數(shù)組,比如這個:

// 將一個一維數(shù)組 'icons' 根據(jù) 'rowLength' 拆分放入數(shù)組中
function splitIntoRows(icons, rowLength) {
  var rows = [];
  for (var i = 0; i < icons.length; i += rowLength) {
    rows.push(icons.slice(i, i + rowLength));
  }
  return rows;
}

生成器則讓這個代碼稍微短一些:

function* splitIntoRows(icons, rowLength) {
  for (var i = 0; i < icons.length; i += rowLength) {
    yield icons.slice(i, i + rowLength);
  }
}

唯一的不同之處在于,函數(shù)返回的是一個迭代器而不是將所有結(jié)果計算好一次性返回整個數(shù)組。結(jié)果是按需逐一計算出來的。

  • 異常大小的結(jié)果。你無法創(chuàng)建一個無窮大小的數(shù)組,但是可以返回一個可以生成無窮序列的生成器,并且每次調(diào)用都可以從中獲取任意數(shù)量的值。

  • 重構(gòu)復雜的循環(huán)。你有寫過又大又丑的函數(shù)嗎?想要把它拆分為兩個更為簡單的部分嗎?相信生成器會成為你重構(gòu)工具箱中的一把利刃。當面對一個復雜的循環(huán)時,你拆分出那段產(chǎn)生數(shù)據(jù)的代碼,然后將其變成一個獨立的生成器函數(shù)。然后用for 修改循環(huán) (var data of myNewGenerator(args))。

  • 創(chuàng)建可迭代的工具。ES6 沒有提供一個可以用于過濾、映射以及針對任意數(shù)據(jù)集進行操作的擴展庫。但是借助生成器,就可以用很少幾行代碼構(gòu)建這一類工具。

例如,假設(shè)你需要一個類似 Array.prototype.filter 的應用于 DOM NodeLists 而不僅僅是數(shù)組的工具。很簡單:

function* filter(test, iterable) {
  for (var item of iterable) {
    if (test(item))
      yield item;
  }
}

那么生成器真的有用嗎?當然。這是極其簡單的、用于實現(xiàn)自定義迭代器的方法,而且根據(jù) ES6 的標準,迭代器是最新的用于數(shù)據(jù)和循環(huán)的標準。

但生成器所能做的并不僅僅局限于此,也許這也不是生成器可以做的最重要的事。

生成器與異步代碼

這是一段我之前寫的代碼:

          };
        })
      });
    });
  });
});

也許你也在自己的代碼中見到過類似這樣的部分。異步的 API 通常需要一個回調(diào),也就意味著沒做一些事情就需要編寫一個額外的異步函數(shù)。所以如果你做了三件事,就會看到有三層縮進的代碼而非三行。

下面也是我曾寫過的一段代碼:

}).on('close', function () {
  done(undefined, undefined);
}).on('error', function (error) {
  done(error);
});

異步接口有錯誤處理的機制,而沒有異常處理。對于不同的接口而言,有著不同的約定俗成。就其中大部分而言,錯誤是默認被靜默拋出的。而其中有一些的成功提示都是默認的。

以上這些問題都是我們?yōu)楫惒骄幊趟冻龅拇鷥r。我們正在慢慢接受異步代碼比如其等效的同步代碼簡潔美觀的事實。

生成器則給了人們不用這樣書寫代碼的新的希望。

Q.async() 是一個實驗性的結(jié)合 promise 使用生成器來生成與異步代碼等效的同步代碼的庫。例如:

// 制造“噪音”的同步代碼
function makeNoise() {
  shake();
  rattle();
  roll();
}

// 制造“噪音”的異步代碼
// 當我們產(chǎn)生“噪音”后
// 返回一個 resolved 狀態(tài)的 Promise 對象
function makeNoise_async() {
  return Q.async(function* () {
    yield shake_async();
    yield rattle_async();
    yield roll_async();
  });
}

上述兩段代碼最主要的區(qū)別就在于異步版本的代碼必須在調(diào)用異步函數(shù)的地方添加 yield 關(guān)鍵詞。

在 Q.async 版本的代碼中增加一個 if 或者 try/catch 語句與添加到普通的同步版本中幾乎一樣。相比其他的編寫異步代碼的方式,這種方式更讓人感覺不是在學習一門完全新的語言。

如果你已經(jīng)到了這一步,可以嘗試欣賞一下 James Long 的關(guān)于這個話題一篇比較詳細的文章

所以生成器指明了一種更為適合人類大腦的新的異步編程模型。相關(guān)的工作還在繼續(xù)進行當中。相比其他東西,更好的語法會更有幫助。一種構(gòu)建于 promise 和 generator 之上的更好的異步函數(shù)提案,將會在 ES7 中出現(xiàn),這一提案借鑒了 C#中一些相似的特性。

什么時候可以使用這些瘋狂的特性

在服務(wù)端,已經(jīng)可以在 io.js (或者用 --harmony 啟動的 Node) 中使用 ES6 的生成器。

而在瀏覽器中,目前為止只有 Firefox27+ 和 Chrome 39+ 支持 ES6 的生成器。若要在瀏覽器中使用之,可以使用 Babel 或者 Traceur 將 ES6 的代碼轉(zhuǎn)換為瀏覽器友好的 ES5代碼。

最初,Brendan Eich 借鑒 Python 實現(xiàn)了 JavaScript 中的生成器,而 Python 中的生成器則是受 Icon 啟發(fā)實現(xiàn)的。他們早在 2006年發(fā)布的 Firefox 2.0中就實現(xiàn)了這一特性。然而,通向標準化的路是崎嶇的,一路上語法與行為都發(fā)生了不少的變化。ES6 生成器在 Firefox 和 Chrome 中都是由編譯器黑客 Andy Wingo 實現(xiàn)的。這一工作由Bloomberg贊助。

yield;

關(guān)于生成器還有很多內(nèi)容,我們沒有提到 .throw().return() 方法,.next() 方法的可選參數(shù)以及 yield* 表達式語法。但是我認為這篇文章到現(xiàn)在已經(jīng)足夠長,看著可能少許有點累了。就像生成器他們一樣,我們需要稍作歇息,之后找個時間繼續(xù)講解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容