ES6——生成器

什麼是生成器?

咱們先從下面的這裏例子開始。html

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!";
}

這段代碼是一個對話貓,這多是當前網絡上最重要的一類應用。程序員

這個在必定程度上看起來像一個函數,對不?這就被稱爲生成器函數,同時它與函數之間也有不少類似之處。可是你一會兒就能發現兩個不一樣之處:編程

普通的函數使用function做爲開始。生成器函數以function*開始。 在一個生成器函數中,yield是一個關鍵字,語法和return很類似。 區別在於,一個函數(甚至是生成器函數),只能返回一次,可是一個生成器函數可以yield不少次。 yield表達式暫停生成器的運行,而後它可以在以後從新被使用。 就是這樣的,以上就是普通的函數和生成器函數之間的大區別。普通的函數不能本身暫停。然而生成器函數能夠本身暫停運行。數組

生成器的用處

當你調用quips()生成器函數時將會發生什麼?瀏覽器

> 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 }

你可能很是習慣於普通的函數以及他們的表現。當你調用他們的時候,他們當即開始運行,當遇到 return或者throw的時候,他們中止運行。任何一個JS程序員都很是習慣於上述的過程。服務器

調用一個生成器看起來是同樣的:quips(」jorendorff」)。 可是當你調用一個生成器,他還不開始運行。反而,它返回一個暫停的生成器對象(在上述的例子中被稱爲iter)。你能夠認爲這個生成器對象是一個函數調用,暫時中止。特別的是,其在生成器函數一開始就中止了,在運行代碼的第一行以前。網絡

每次你調用生成器對象的.next()方法,函數將其本身解凍並運行直到其到達下一個yield表達式。併發

這就是上面代碼中咱們爲何要調用iter.next(),調用後咱們得到一個不一樣的字符串值。這些值都是由quips()裏的yield表達式產生的。異步

在最後一個iter.next( )調用中,咱們最後結束了生成器函數,因此結果的.done領域的值爲true。 一個函數的結束就像是返回undefined,並且這也是爲何結果的.value領域是不肯定的。async

如今多是一個好機會來返回到上面的對話貓的例子那頁面上,同時真正地能夠玩轉代碼。嘗試着在一個循環中加入一個yield。這會發生什麼呢?

在技術層面上,每一次一個生成器進行yield操做,其堆棧楨,包括局部變量,參數,臨時值,以及在生成器中的執行的當前位置,被從棧中刪除。然而,生成器對象保有這個堆棧幀的引用(或者是副本)。所以,接下來的調用.next( )能夠從新激活它並繼續執行。

值得指出的是,生成器都沒有線程。在能使用線程的語言中,多份代碼能夠在同一時間運行,這一般致使了競爭條件,非肯定性和很是很是好的性能。生成器和這徹底不一樣。當生成器運行時,它與調用者運行在同一個線程中。執行的順序是連續且肯定的,並永遠不會併發。不一樣於系統線程,生成器只會在代碼中用yield標記的地方纔會懸掛。

好了。咱們知道生成器是什麼了。咱們已經看到了生成器器運行,暫停,而後恢復執行。如今的大問題是,這樣奇怪的能力怎麼多是有用的呢?

生成器是迭代器,咱們已經看到了ES6的迭代器不僅是一個簡單的內置類。他們是語言的擴展點。你能夠經過實現兩種方法來建立你本身的迭代器,這兩種方法是:Symbol.iterator和.next()。

可是,實現接口老是至少仍是有一點工做量的。讓咱們來看看一個迭代器實如今實踐中看起來是什麼樣的。由於是一個例子,讓咱們使用一個簡單的範圍(range)迭代器,只簡單的從一個數字數到另外一個數字,就像一個老式的C語言的for(;;)循環。

// 這個應該三次發出「叮」的聲音

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};
    }
  }
}
// 返回一個新的從「開始」數到「結束」的迭代器。

function range(start, stop) {
  return new RangeIterator(start, stop);
}

在實際運行中看這段代碼(http://codepen.io/anon/pen/NqGgOQ)。

這就是像是在Java或Swift語言裏實現一個迭代器。它不是那麼糟糕。但它也並非那麼簡單。在這個代碼中有沒有任何錯誤?這就很差說了。它看起來徹底不像咱們想在這裏模仿的原來的for(;;)循環:迭代器協議迫使咱們拋棄了循環。

在這一點上,你可能會對迭代器不太熱情。他們可能對使用來講很棒,但他們彷佛很難實現。

你可能不會建議咱們只是爲了簡單的建立迭代器,而在JS語言中引進一個複雜的新的控制流結構。可是,由於咱們確實有生成器,咱們能在這裏使用它們嗎?讓咱們試一試:

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

在這裏看代碼的具體運行(http://codepen.io/anon/pen/mJewga)。

上述的四行的生成器是對先前range()的二十三行的實現的一個直接替代,包括了整個RangeIterator類。這多是由於生成器是迭代器。全部的生成器都有一個內置的對.next()已經Symbol.iterator方法的實現。

不使用生成器來實現迭代器就像是被強迫用被動語氣寫一封很長的郵件。原本想簡單地想表達你的意思,可能到最後你說的會變得至關使人費解。RangeIterator是很長且怪異的,由於它必須不使用循環語法來描述一個循環的功能。生成器是答案。

咱們還能如何使用生成器做爲迭代器的能力?

使對象可迭代。只要寫一個迭代器函數來一直調用this,在其出現的地方生成每個值。而後以該對象的[Symbol.iterator]方法來安裝該生成器函數。

簡化數組構建函數。實現一個函數,每當其被調用就會返回一個數組,以下面的這個例子:

// 將一維數組'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循環(myNewGenerator(args)的var數據)。

使用迭代的工具。ES6不提供擴展的庫來進行過濾,映射以及通常能夠訪問任意可迭代的數據集。但生成器是偉大的,你只須要用幾行代碼就能夠構建你須要的工具。舉個例子說,假設你須要一個東西等同於Array.prototype.filter,其是在DOM NodeLists上工做的,不僅是一個數組。這用代碼來實現就是小意思:

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

這個就是爲何生成器如此有用嗎?固然。他們是要實現自定義的迭代器的簡單的方法。同時,迭代器在整個ES6中是用於數據和循環的新標準。

可是,這還不是生成器能作的事情的所有。這甚至有可能不是他們作的最重要的事情。

生成器和異步代碼

這裏是一個 JS 代碼,我寫了一個 while 的 back 部分。

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

可能這看起來和你代碼的一部分比較相像。異步API一般狀況下須要一個回調,這就意味着你作一些事情時要寫一個額外的匿名函數。因此若是你有一部分代碼作三件事情,而不是三行代碼,你是在看三個縮進層次的代碼。

這裏有一些我已經寫好的 JS 代碼:

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

異步API有錯誤處理的約定,但不是使用異常。不一樣的API有不一樣的約定。在他們中的大多數中,錯誤在默認狀況下被默默地刪除。其中有一些,即便是普通的圓滿完成,在默認狀況下都會被刪除。

直到如今,這些問題都只是簡單的轉畫爲咱們進行異步編程的代價了。咱們已經開始接受異步代碼了,他們只是看起來不是像同步代碼那樣美好和簡單。

生成器提供了新的但願,能夠不用這樣作的。

Q.async()是一個試驗性的嘗試。其使用迭代器來生成相似於同步代碼的異步代碼。舉個例子:

// 
function makeNoise() {
  shake();
  rattle();
  roll();
}

// Asynchronous code to make some noise.
// Returns a Promise object that becomes resolved
// when we're done making noise.
function makeNoise_async() {
  return Q.async(function* () {
    yield shake_async();
    yield rattle_async();
    yield roll_async();
  });
}

主要的區別在於,異步版本必須在每一個須要調用異步函數的地方添加yield關鍵字。

在Q.async版本中添加一點小東西,如if語句或try/catch塊,與在普通同步版本中添加是徹底相同的。相比於編寫異步代碼的其餘方式,有種不是在學習一個全新的語言的感受。

因此生成器指出了一個更適合人類大腦的新的異步編程模型。這項工做正在進行中。除其餘事項外,更好的語法可能有所幫助。

異步函數的提出,創建在雙方的承諾和生成器的基礎上,並從在C#相似的功能中採起靈感,這些都是ES7要作的事情。

我何時能夠用這瘋狂的東西?

在服務器端,咱們如今能夠在io.js中使用 ES6 生成器。若是你啓用--harmony選項,咱們在Node中可也已使用ES6生成器。

在瀏覽器中,現今只有火狐27版本以上以及谷歌瀏覽器39版本以上的支持ES6生成器。爲了在現今的網絡上面使用生成器,你將須要使用Babel或者Traceur來將你的ES6的代碼翻譯爲網絡友好的ES5代碼。

一些重要的事件值得了解

生成器是由布倫丹·艾希首次在JS上實現的。布倫丹·艾希的設計是牢牢跟隨由Icon啓發的Python生成器。他們早在2006年就運用在火狐2.0版本上了。可是標準化的道路是崎嶇不平的,並且語法和行爲在這個過程當中改變了不少。ES6生成器是由編程黑客溫格安迪在火狐瀏覽器和谷歌瀏覽器中實現的。這項工做是由Bloomberg贊助的。

yield

關於生成器還有更多的說法。咱們沒有包含.throw()和.return()方法,可選的參數.next(),或yield*表達式語法。但我認爲這個帖子已經很長了,且如今已經足夠撲朔迷離了。像生成器自己,咱們應該停下來休息一下。

轉載:http://www.html-js.com

相關文章
相關標籤/搜索