深入淺出ES6(三):生成器 Generators

jopen 9年前發布 | 33K 次閱讀 JavaScript開發 Generators

編者按:ECMAScript 6已經正式發布了,作為它最重要的方言,Javascript也即將迎來語法上的重大變革,InfoQ特開設“深入淺出ES6”專欄,來看一下ES6將給我們帶來哪些新內容。本專欄文章來自Mozilla Web開發者博客,由作者授權翻譯并發布。

今天的這篇文章令我感到非常興奮,我們將一起領略ES6中最具魔力的特性。

為什么說是“最具魔力的”?對于初學者來說,此特性與JS之前已有的特性截然不同,可能會覺得有點晦澀難懂。但是,從某種意義上來說,它使語言內部的常態行為變得更加強大,如果這都不算有魔力,我不知道還有什么能算。

不僅如此,此特性可以極大地簡化代碼,它甚至可以幫助你逃離“回調地獄”。

既然新特性如此神奇,那么就一起深入了解它的魔力吧!

ES6生成器(Generators)簡介

什么是生成器?

我們從一個示例開始:

function* quips(name) {
  yield "你好 " + name + "!";
  yield "希望你能喜歡這篇介紹ES6的譯文";
  if (name.startsWith("X")) {
    yield "你的名字 " + name + "  首字母是X,這很酷!";
  }
  yield "我們下次再見!";
}

這是一只會說話的貓,這段代碼很可能代表著當今互聯網上最重要的一類應用。(試著點擊這個鏈接,與這只貓互動一下,如果你感到有些困惑,回到這里繼續閱讀)。

這段代碼看起來很像一個函數,我們稱之為生成器函數,它與普通函數有很多共同點,但是二者有如下區別:

  • 普通函數使用function聲明,而生成器函數使用function*聲明。
  • 在生成器函數內部,有一種類似return的語法:關鍵字yield。二者的區別是,普通函數只可以return一次,而生成器函數可以yield多次(當然也可以只yield一次)。在生成器的執行過程中,遇到yield表達式立即暫停,后續可恢復執行狀態。

這就是普通函數和生成器函數之間最大的區別,普通函數不能自暫停,生成器函數可以。

生成器做了什么?

當你調用quips()生成器函數時發生了什么?

> var iter = quips("jorendorff");
  [object Generator]
> iter.next()
  { value: "你好 jorendorff!", done: false }
> iter.next()
  { value: "希望你能喜歡這篇介紹ES6的譯文", done: false }
> iter.next()
  { value: "我們下次再見!", done: false }
> iter.next()
  { value: undefined, done: true }

你大概已經習慣了普通函數的使用方式,當你調用它們時,它們立即開始運行,直到return返回值或throw一個異常時才退出執行,作為JS程序員你一定深諳此道。

生成器調用看起來非常類似:quips("jorendorff")。但是,當你調用一個生成器時,它并非立即執行,而是返回一個已暫停的生成器對象(上述實例代碼中的iter)。你可將這個生成器對象視為一次函數調用,只不過立即凍結了,它恰好在生成器函數的最頂端的第一行代碼之前凍結了。

每當你調用生成器對象的.next()方法時,函數調用將其自身解凍并一直運行到下一個yield表達式,再次暫停。

這也是在上述代碼中我們每次都調用iter.next()的原因,我們獲得了quips()函數體中yield表達式生成的不同的字符串值。

調用最后一個iter.next()時,我們最終抵達生成器函數的末尾,所以返回結果中done的值為true。抵達函數的末尾意味著沒有返回值,所以返回結果中value的值為undefined。

現在回到會說話的貓的demo頁面,嘗試在循環中加入一個yield,會發生什么?

如果用專業術語描述,每當生成器執行yields語句,生成器的堆棧結構(本地變量、參數、臨時值、生成器內部當前的執行位置)被移出堆棧。然而,生成器對象保留了對這個堆棧結構的引用(備份),所以稍后調用.next()可以重新激活堆棧結構并且繼續執行。

值得特別一提的是,生成器不是線程,在支持線程的語言中,多段代碼可以同時運行,通通常導致競態條件和非確定性,不過同時也帶來不錯的性能。生成器則完全不同。當生成器運行時,它作為調用者運行在單一線程中,擁有確定的連續執行順序,永不并發。與系統線程不同的是,生成器只有在其函數體內標記為yield的點才會暫停。

現在,我們了解了生成器的原理,領略過生成器的運行、暫停恢復運行的不同狀態。那么,這些奇怪的功能究竟有何用處?

生成器是迭代器!

上周,我們學習了ES6的迭代器,它是ES6中獨立的內建類,同時也是語言的一個擴展點,通過實現[Symbol.iterator]()和.next()兩個方法你就可以創建自定義迭代器。

實現一個接口不是一樁小事,我們一起實現一個迭代器。舉個例子,我們創建一個簡單的range迭代器,它可以簡單地將兩個數字之間的所有數相加。首先是傳統C的for(;;)循環:

// 應該彈出三次 "ding"
for (var value of range(0, 3)) {
  alert("Ding! at floor #" + value);
}

使用ES6的類的解決方案(如果不清楚語法細節,無須擔心,我們將在接下來的文章中為你講解):

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到stop計數。
function range(start, stop) {
  return new RangeIterator(start, stop);
}

查看代碼運行情況。

這里的實現類似JavaSwift中的迭代器,不是很糟糕,但也不是完全沒有問題。我們很難說清這段代碼中是否有bug,這段代碼看起來完全不像我們試圖模仿的傳統for (;;)循環,迭代器協議迫使我們拆解掉循環部分。

此時此刻你對迭代器可能尚無感覺,他們用起來很酷,但看起來有些難以實現。

你大概不會為了使迭代器更易于構建從而建議我們為JS語言引入一個離奇古怪又野蠻的新型控制流結構,但是既然我們有生成器,是否可以在這里應用它們呢?一起嘗試一下:

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

查看代碼運行情況。

以上4行代碼實現的生成器完全可以替代之前引入了一整個RangeIterator類的23行代碼的實現。可行的原因是:生成器是迭代器。所有的生成器都有內建.next()和[Symbol.iterator]()方法的實現。你只須編寫循環部分的行為。

我們都非常討厭被迫用被動語態寫一封很長的郵件,不借助生成器實現迭代器的過程與之類似,令人痛苦不堪。當你的語言不再簡練,說出的話就會變得難以理解。RangeIterator的實現代碼很長并且非常奇怪,因為你需要在不借助循環語法的前提下為它添加循環功能的描述。所以生成器是最好的解決方案!

我們如何發揮作為迭代器的生成器所產生的最大效力?

l 使任意對象可迭代。編寫生成器函數遍歷這個對象,運行時yield每一個值。然后將這個生成器函數作為這個對象的[Symbol.iterator]方法。

l 簡化數組構建函數。假設你有一個函數,每次調用的時候返回一個數組結果,就像這樣:

// 拆分一維數組icons
// 根據長度rowLength
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);
  }
}

行為上唯一的不同是,傳統寫法立即計算所有結果并返回一個數組類型的結果,使用生成器則返回一個迭代器,每次根據需要逐一地計算結果。

  • 獲取異常尺寸的結果。你無法構建一個無限大的數組,但是你可以返回一個可以生成一個永無止境的序列的生成器,每次調用可以從中取任意數量的值。
  • 重構復雜循環。你是否寫過又丑又大的函數?你是否愿意將其拆分為兩個更簡單的部分?現在,你的重構工具箱里有了新的利刃——生成器。當你面對一個復雜的循環時,你可以拆分出生成數據的代碼,將其轉換為獨立的生成器函數,然后使用for (var data of myNewGenerator(args))遍歷我們所需的數據。
  • 構建與迭代相關的工具。ES6不提供用來過濾、映射以及針對任意可迭代數據集進行特殊操作的擴展庫。借助生成器,我們只須寫幾行代碼就可以實現類似的工具。

舉個例子,假設你需要一個等效于Array.prototype.filter并且支持DOM NodeLists的方法,可以這樣寫:

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

你看,生成器魔力四射!借助它們的力量可以非常輕松地實現自定義迭代器,記住,迭代器貫穿ES6的始終,它是數據和循環的新標準。

以上只是生成器的冰山一角,最重要的功能請繼續觀看!生成器和異步代碼

這是我在一段時間以前寫的一些JS代碼

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

可能你已經在自己的代碼中見過類似的片段,異步API通常需要一個回調函數,這意味著你需要為每一次任務執行編寫額外的異步函數。所以如果你有一段代碼需要完成三個任務,你將看到類似的三層級縮進的代碼,而非簡單的三行代碼。

后來我就這樣寫了:

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

異步API擁有錯誤處理規則,不支持異常處理。不同的API有不同的規則,大多數的錯誤規則是默認的;在有些API里,甚至連成功提示都是默認的。

這些是到目前為止我們為異步編程所付出的代價,我們正慢慢開始接受異步代碼不如等效同步代碼美觀又簡潔的這個事實。

生成器為你提供了避免以上問題的新思路。

實驗性的Q.async()嘗試結合promises使用生成器產生異步代碼的等效同步代碼。舉個例子:

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

// 制造一些噪音的異步代碼。
// 返回一個Promise對象
// 當我們制造完噪音的時候會變為resolved
function makeNoise_async() {
  return Q.async(function* () {
    yield shake_async();
    yield rattle_async();
    yield roll_async();
  });
}

二者主要的區別是,異步版本必須在每次調用異步函數的地方添加yield關鍵字。

在Q.async版本中添加一個類似if語句的判斷或try/catch塊,如同向同步版本中添加類似功能一樣簡單。與其它異步代碼編寫方法相比,這種方法更自然,不像是學一門新語言一樣辛苦。

如果你已經看到這里,你可以試著閱讀來自James Long的更深入地講解生成器的文章

生成器為我們提供了一個新的異步編程模型思路,這種方法更適合人類的大腦。相關工作正在不斷展開。此外,更好的語法或許會有幫助,ES7中有一個有關異步函數的提案,它基于promises和生成器構建,并從C#相似的特性中汲取了大量靈感。

如何應用這些瘋狂的新特性?

在服務器端,現在你可以在io.js中使用ES6(在Node中你需要使用--harmony這個命令行選項)。

在瀏覽器端,到目前為止只有Firefox 27+和Chrome 39+支持了ES6生成器。如果要在web端使用生成器,你需要使用BabelTraceur來將你的ES6代碼轉譯為Web友好的ES5。

起初,JS中的生成器由Brendan Eich實現,他的設計參考了Python生成器,而此Python生成器則受到Icon的啟發。他們早在2006年就在Firefox 2.0中移植了相關代碼。但是,標準化的道路崎嶇不平,相關語法和行為都在原先的基礎上有所改動。Firefox和Chrome中的ES6生成器都是由編譯器hacker Andy Wingo實現的。這項工作由彭博贊助支持(沒聽錯,就是大名鼎鼎的那個彭博!)。

yield;

生成器還有更多未提及的特性,例如:.throw()和.return()方法、可選參數.next()、yield*表達式語法。由于行文過長,估計觀眾老爺們已然疲乏,我們應該學習一下生成器,暫時yield在這里,剩下的干貨擇機為大家獻上。

下一次,我們變換一下風格,由于我們接連搬了兩座大山:迭代器和生成器,下次就一起研究下不會改變你編程風格的ES6特性好不?就是一些簡單又實用的東西,你一定會喜笑顏開噠!你還別說,在什么都要“微”一下的今天,ES6當然要有微改進了!

下回預告:ES6模板字符串深度解析,每天都會寫的代碼!觀眾老爺們記得回來哦!我會想你們的!


感謝徐川對本文的審校。

來自:http://www.infoq.com/cn/articles/es6-in-depth-generators

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!