DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> JavaScript基礎知識 >> 詳解JavaScript ES6中的Generator
詳解JavaScript ES6中的Generator
編輯:JavaScript基礎知識     

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

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

不僅如此,該特性還可以簡化程序代碼,將復雜的“回調堆棧”改成直線執行的形式。

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

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

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

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

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

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

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

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

我們對普通函數的行為非常熟悉,函數被調用時就立即執行,直到函數返回或拋出一個異常,這是所有 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 (;;) 循環。

// This should "ding" three times
for (var value of range(0, 3)) {
 alert("Ding! at floor #" + value);
}
 
// This should "ding" three times
for (var value of range(0, 3)) {
 alert("Ding! at floor #" + value);
}

現在有一個解決方案,就是使用 ES6 的類。(如果你對 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};
  }
 }
}

// Return a new iterator that counts up from 'start' to 'stop'.
function range(start, stop) {
 return new RangeIterator(start, stop);
}
 
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};
  }
 }
}
 
// Return a new iterator that counts up from 'start' to 'stop'.
function range(start, stop) {
 return new RangeIterator(start, stop);
}

查看該 DEMO。

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

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

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

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

查看該 DEMO。

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

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

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

    使任何對象可遍歷 — 編寫一個 Genetator 函數去遍歷 this,每遍歷到一個值就 yield 一下,然後將該 Generator 函數作為要遍歷的對象上的 [Symbol.iterator] 方法的實現。
    簡化返回數組的函數 — 假如有一個每次調用時都返回一個數組的函數,比如:

// Divide the one-dimensional array 'icons'
// into arrays of length '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;
}

 
// Divide the one-dimensional array 'icons'
// into arrays of length '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;
}

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

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

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

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

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

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

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

所以,Generator 很實用吧?當然,這是實現自定義迭代器最簡單直接的方式,並且,在 ES6 中,迭代器是數據集和循環的新標准。

但,這還不是 Generator 的全部功能。
異步代碼

異步 API 通常都需要一個回調函數,這意味著每次你都需要編寫一個匿名函數來處理異步結果。如果同時處理三個異步事務,我們看到的是三個縮進層次的代碼,而不僅僅是三行代碼。

看下面代碼:

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

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

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

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

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

例如:

// Synchronous code to make some noise.
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();
 });
}
 
// Synchronous code to make some noise.
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 異常處理,就和在同步代碼中的方式一樣,與其他編寫異步代碼的方式相比,減少了很多學習成本。

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 將改變我們的生活,看似簡單的特性,卻有極大的用處。

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved