[譯] 什麼是 JavaScript 生成器?如何使用生成器?

在本文中,咱們將瞭解 ECMAScript 6 中引入的生成器(Generator)。先看一看它到底是什麼,而後用幾個示例來講明它的用法。javascript

什麼是 JavaScript 生成器?

生成器是一種能夠用來控制迭代器(iterator)的函數,它能夠隨時暫停,並能夠在任意時候恢復。前端

上面的描述無法說明什麼,讓咱們來看一些例子,解釋什麼是生成器,以及生成器與 for 循環之類的迭代器有什麼區別。java

下面是一個 for 循環的例子,它會在執行後馬上返回一些值。這段代碼其實就是簡單地生成了 0-5 這些數字。android

for (let i = 0; i < 5; i += 1) {
  console.log(i);
}
// 它將會馬上返回 0 -> 1 -> 2 -> 3 -> 4
複製代碼

如今看看生成器函數。ios

function * generatorForLoop(num) {
  for (let i = 0; i < num; i += 1) {
    yield console.log(i);
  }
}

const genForLoop = generatorForLoop(5);

genForLoop.next(); // 首先 console.log - 0
genForLoop.next(); // 1
genForLoop.next(); // 2
genForLoop.next(); // 3
genForLoop.next(); // 4
複製代碼

它作了什麼?它實際上只是對上面例子中的 for 循環作了一點改動,但產生了很大的變化。這種變化是由生成器最重要的特性形成的 —— 只有在須要的時候它纔會產生下一個值,而不會一次性產生全部的值。在某些情景下,這種特性十分方便。git

生成器語法

如何定義一個生成器函數呢?下面列出了各類可行的定義方法,不過萬變不離其宗的是在函數關鍵詞後加上一個星號。github

function * generator () {}
function* generator () {}
function *generator () {}

let generator = function * () {}
let generator = function* () {}
let generator = function *() {}

let generator = *() => {} // SyntaxError
let generator = ()* => {} // SyntaxError
let generator = (*) => {} // SyntaxError
複製代碼

如上面的例子所示,咱們並不能使用箭頭函數來建立一個生成器。後端

下面將生成器做爲方法(method)來建立。定義方法與定義函數的方式是同樣的。bash

class MyClass {
  *generator() {}
  * generator() {}
}

const obj = {
  *generator() {}
  * generator() {}
}
複製代碼

yield

如今讓咱們一塊兒看看新的關鍵詞 yield。它有些相似 return,但又不徹底相同。return 會在完成函數調用後簡單地將值返回,在 return 語句以後你沒法進行任何操做。dom

function withReturn(a) {
  let b = 5;
  return a + b;
  b = 6; // 不可能從新定義 b 了
  return a * b; // 這兒新的值沒可能返回了
}

withReturn(6); // 11
withReturn(6); // 11
複製代碼

yield 的工做方式卻不一樣。

function * withYield(a) {
  let b = 5;
  yield a + b;
  b = 6; // 在第一次調用後仍能夠從新定義變量
  yield a * b;
}

const calcSix = withYield(6);

calcSix.next().value; // 11
calcSix.next().value; // 36
複製代碼

yield 返回的值只會返回一次,當你再次調用同一個函數的時候,它會執行至下一個 yield 語句處(譯者注:前面的 yield 再也不返回東西了)。

在生成器中,咱們一般會在輸出時獲得一個對象。這個對象有兩個屬性:valuedone。如你所想,value 爲返回值,done 則會顯示生成器是否完成了它的工做。

function * generator() {
  yield 5;
}

const gen = generator();

gen.next(); // {value: 5, done: false}
gen.next(); // {value: undefined, done: true}
gen.next(); // {value: undefined, done: true} - 以後的任何調用都會返回相同的結果
複製代碼

在生成器中,不只可使用 yield,也可使用 return 來返回一樣的對象。可是,在函數執行到第一個 return 語句的時候,生成器將結束它的工做。

function * generator() {
  yield 1;
  return 2;
  yield 3; // 到不了這個 yield 了
}

const gen = generator();

gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: true}
gen.next(); // {value: undefined, done: true}
複製代碼

yield 委託迭代

帶星號的 yield 能夠將它的工做委託給另外一個生成器。經過這種方式,你就能將多個生成器鏈接在一塊兒。

function * anotherGenerator(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function * generator(i) {
  yield* anotherGenerator(i);
}

var gen = generator(1);

gen.next().value; // 2
gen.next().value; // 3
gen.next().value; // 4
複製代碼

在開始下一節前,咱們先觀察一個第一眼看上去比較奇特的行爲。

下面是正常的代碼,不會報出任何錯誤,這代表 yield 能夠在 next() 方法調用後返回傳遞的值:

function * generator(arr) {
  for (const i in arr) {
    yield i;
    yield yield;
    yield(yield);
  }
}

const gen = generator([0,1]);

gen.next(); // {value: "0", done: false}
gen.next('A'); // {value: undefined, done: false}
gen.next('A'); // {value: "A", done: false}
gen.next('A'); // {value: undefined, done: false}
gen.next('A'); // {value: "A", done: false}
gen.next(); // {value: "1", done: false}
gen.next('B'); // {value: undefined, done: false}
gen.next('B'); // {value: "B", done: false}
gen.next('B'); // {value: undefined, done: false}
gen.next('B'); // {value: "B", done: false}
gen.next(); // {value: undefined, done: true}
複製代碼

在這個例子中,你能夠看到 yield 默認是 undefined,但若是咱們在調用 yield 時傳遞了任何值,它就會返回咱們傳入的值。咱們將很快利用這個特性。

初始化與方法

生成器是能夠被複用的,可是你須要對它們進行初始化。還好初始化的方法十分簡單。

function * generator(arg = 'Nothing') {
  yield arg;
}

const gen0 = generator(); // OK
const gen1 = generator('Hello'); // OK
const gen2 = new generator(); // 不 OK

generator().next(); // 能夠運行,但每次都會從頭開始運行
複製代碼

如上所示,gen0gen1 不會互相影響,gen2 徹底不會運行(會報錯)。所以初始化對於保證程序流程的狀態是十分重要的。

下面讓咱們一塊兒看看生成器給咱們提供的方法。

next() 方法

function * generator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = generator();

gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: false}
gen.next(); // {value: 3, done: false}
gen.next(); // {value: undefined, done: true} 以後全部的 next 調用都會返回一樣的輸出
複製代碼

這是最經常使用的方法。它每次被調用時都會返回下一個對象。在生成器工做結束時,next() 會將 done 屬性設爲 truevalue 屬性設爲 undefined

咱們不只能夠用 next() 來迭代生成器,還能夠用 for of 循環來一次獲得生成器全部的值(而不是對象)。

function * generator(arr) {
  for (const el in arr)
    yield el;
}

const gen = generator([0, 1, 2]);

for (const g of gen) {
  console.log(g); // 0 -> 1 -> 2
}

gen.next(); // {value: undefined, done: true}
複製代碼

但它不適用於 for in 循環,而且不能直接用數字下標來訪問屬性:generator[0] = undefined

return() 方法

function * generator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = generator();

gen.return(); // {value: undefined, done: true}
gen.return('Heeyyaa'); // {value: "Heeyyaa", done: true}

gen.next(); // {value: undefined, done: true} - 在 return() 以後的全部 next() 調用都會返回相同的輸出

複製代碼

return() 將會忽略生成器中的任何代碼。它會根據傳值設定 value,並將 done 設爲 true。任何在 return() 以後進行的 next() 調用都會返回 done 屬性爲 true 的對象。

throw() 方法

function * generator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = generator();

gen.throw('Something bad'); // 會報錯 Error Uncaught Something bad
gen.next(); // {value: undefined, done: true}
複製代碼

throw() 作的事很是簡單 —— 就是拋出錯誤。咱們能夠用 try-catch 來處理。

自定義方法的實現

因爲咱們沒法直接訪問 Generator 的 constructor,所以如何增長新的方法須要另外說明。下面是個人方法,你也能夠用不一樣的方式實現:

function * generator() {
  yield 1;
}

generator.prototype.__proto__; // Generator {constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, Symbol(Symbol.toStringTag): "Generator"}

// 因爲 Generator 不是一個全局變量,所以咱們只能這麼寫:
generator.prototype.__proto__.math = function(e = 0) {
  return e * Math.PI;
}

generator.prototype.__proto__; // Generator {math: ƒ, constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, …}

const gen = generator();
gen.math(1); // 3.141592653589793
複製代碼

生成器的用途

在前面,咱們用了已知迭代次數的生成器。但若是咱們不知道要迭代多少次會怎麼樣呢?爲了解決這個問題,須要在生成器函數中建立一個無限循環。下面以一個會返回隨機數的函數爲例進行演示:

function * randomFrom(...arr) {
  while (true)
    yield arr[Math.floor(Math.random() * arr.length)];
}

const getRandom = randomFrom(1, 2, 5, 9, 4);

getRandom.next().value; // 返回隨機的一個數
複製代碼

這是個簡單的例子。下面來舉一些更復雜的函數爲例,咱們要寫一個節流(throttle)函數。若是你還不知道節流函數是什麼,請參閱這篇文章

function * throttle(func, time) {
  let timerID = null;
  function throttled(arg) {
    clearTimeout(timerID);
    timerID = setTimeout(func.bind(window, arg), time);
  }
  while (true)
    throttled(yield);
}

const thr = throttle(console.log, 1000);

thr.next(); // {value: undefined, done: false}
thr.next('hello'); // 返回 {value: undefined, done: false} ,而後 1 秒後輸出 'hello'
複製代碼

還有沒有更好的利用生成器的例子呢?若是你瞭解遞歸,那你確定聽過斐波那契數列。一般咱們是用遞歸來解決這個問題的,但有了生成器後,能夠這樣寫:

function * fibonacci(seed1, seed2) {
  while (true) {
    yield (() => {
      seed2 = seed2 + seed1;
      seed1 = seed2 - seed1;
      return seed2;
    })();
  }
}

const fib = fibonacci(0, 1);
fib.next(); // {value: 1, done: false}
fib.next(); // {value: 2, done: false}
fib.next(); // {value: 3, done: false}
fib.next(); // {value: 5, done: false}
fib.next(); // {value: 8, done: false}
複製代碼

再也不須要遞歸了!咱們能夠在須要的時候得到數列中的下一個數字。

將生成器用在 HTML 上

既然是討論 JavaScript,那顯然要用生成器來操做下 HTML。

假設有一些 HTML 塊須要處理,可使用生成器來輕鬆實現。(固然除了生成器以外還有不少方法能夠作到)

咱們只須要少量代碼就能完成此需求。

const strings = document.querySelectorAll('.string');
const btn = document.querySelector('#btn');
const className = 'darker';

function * addClassToEach(elements, className) {
  for (const el of Array.from(elements))
    yield el.classList.add(className);
}

const addClassToStrings = addClassToEach(strings, className);

btn.addEventListener('click', (el) => {
  if (addClassToStrings.next().done)
    el.target.classList.add(className);
});
複製代碼

僅有 5 行邏輯代碼。

總結

還有更多使用生成器的方法。例如,在進行異步操做或者按需循環時生成器也很是有用。

我但願這篇文章能幫你更好地理解 JavaScript 生成器。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索