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

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或拋出異常時才退出執行,作爲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在這裏,剩下的乾貨擇機爲大家獻上。

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