【譯】什麼是JavaScript generator 以及如何使用它們

在本文中,咱們將要介紹 ECMAScript 6 中的 generator 是什麼,以及關於它們的使用案例。javascript

什麼是 JavaScript generator

generators 是能夠控制 iterator(迭代器)的函數。並在任什麼時候候均可以暫停和恢復。前端

若是這很差理解,那讓咱們看一些示例,這些示例將解釋 generator 是什麼,以及它和 iterator(迭代器,如 for-loop) 之間的區別。java

這是一個當即輸出值的 for 循環。這段代碼是作什麼的?—— 只是輸出 0 到 5 之間的數git

for (let i = 0; i < 5; i += 1) {
  console.log(i);
}
// 將會當即輸出 0 -> 1 -> 2 -> 3 -> 4
複製代碼

如今讓咱們看看 generator 函數:github

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 循環。可是最主要的變化是,它不會當即執行。這是 generator 中最重要的特性 — 咱們能夠在真正須要下一個值的時候,纔去獲取它,而不是一次得到全部值。在有些狀況下,這會很是方便。dom

generator 語法

如何聲明一個 generator 函數呢?咱們有不少方法能夠實現,可是最主要的是在函數關鍵字以後添加一個星號。異步

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

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

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

從上面的示例中能夠看出,咱們不能使用箭頭函數來建立一個 generator。函數

接下來 —— generator 做爲一種方法。它的聲明方式與函數相同。oop

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

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

Yield

如今,讓咱們來看看新的關鍵字 yield。它有點像 return,但不是。return 只在函數調用以後返回值,return 語句以後不容許你執行任何其餘操做。ui

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 只返回一次值,下次調用 next() 時,它將執行到下一個 yield 語句。

在 generator 中咱們一般都會得到一個對象做爲輸出。它有兩個屬性 valuedone。正如你所想,value 表示返回的值,done 告訴咱們 generator 是否完成了它的工做(是否迭代完成)。

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} - 繼續調用 next() 將返回相同的輸出。
複製代碼

在 generator 中不只可使用 yieldreturn 語句也會返回相同的對象,可是當執行完第一個 retrurn 語句時,迭代就會中止了。

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 delegator

帶星號的 yield 能夠代理執行另外一個 generator。這樣你就能夠根據須要連續調用多個 generator。

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
複製代碼

在咱們詳細介紹 generator 中的方法以前,讓咱們來看看第一次看到會以爲很奇怪的一些行爲。

下面這段代碼向咱們展現了 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}
複製代碼

正如你在這個例子中所看到的,默認狀況下,yieldundefined 的,可是若是咱們傳遞任何值,而且只調用 yield,它將返回咱們傳遞的值。咱們很快就會使用這個功能。

方法和初始化

generator 是可重用的,可是你須要初始化它們,幸運的是,這很是簡單。

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

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

generator().next(); // 這樣也能夠,可是會每一次從都頭開始
複製代碼

gen0gen1 不會相互影響。gen2 不會起做用,甚至會報一個錯誤。合理的初始化一個 generator 對於保持其內部的執行進度很是重要。

如今讓咱們看看 generator 提供給咱們的一些方法。

方法 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() 將返回相同的輸出。
複製代碼

這將會是你最常使用的方法。每次調用它時,它都會爲咱們輸出一個對象。當全部的 yield 表達式被執行完以後,next() 會把屬性 done 設置爲 true,將屬性 value 設置爲 undfined

咱們不只能夠用 next() 來迭代 generator,還能夠用 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 構造函數,所以咱們須要另想辦法來添加自定義方法。 示例是個人辦法,固然你也能夠選擇不一樣的方式。

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
複製代碼

generator 的做用

在前面,咱們用了已知迭代次數的 generator。但若是咱們不知道要迭代多少次會怎麼樣呢?要想解決這個問題,在 generator 函數中建立一個死循環就足夠了。下面是一個返回隨機數的函數的例子:

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

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

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

這很容易,可是對於更復雜的功能,例如節流函數。 若是你不知道它是什麼,請參考這篇文章

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'
複製代碼

還有沒有更好的使用 generator 的例子呢?若是你據說過遞歸,我相信你也據說過斐波那契數列。 一般它是經過遞歸來解決的,可是在 generator 的幫助下咱們能夠這樣寫它:

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}
複製代碼

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

將 generator 用在 HTML 中

既然咱們討論的是 JavaScript,那最顯著的方式就是使用 generator 對 HTML 進行一些操做。

所以,假設咱們有一些 HTML 塊要處理,咱們能夠很容易地經過 generator 實現,但要記住,在不使用 generator 的狀況下,還有不少可能的方法能夠實現這一點。

Using generator-iterator with HTML elements A PEN BY Vlad

咱們只須要不多的代碼就能完成此需求。

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 行邏輯代碼。

總結

使用 generator 還有不少能作的事情。例如,在進行異步操做時,它能夠頗有用。或者是遍歷一個按需項循環。

我但願這篇文章能幫助你更好地理解 JavaScript generator。

查看更多分享,請關注閱文集團前端團隊公衆號:

相關文章
相關標籤/搜索