產生器(Generator)

JavaScript

一般的 function 在呼叫後只會回傳一個值或不回傳任何東西,但產生器(generator)卻可以一個接著一個的回傳(yield)多個值;產生器和迭代器常用來一起處理資料流。

Generator Function

function* 是建立產生器的語法,稱為 generator function。

範例如下,定義一個產生器 generator。

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

當呼叫這個產生器時,它不會執行這個 function,而是會回傳一個產生器物件(generator object)來控制這個 function 的執行,或說是迭代器也可以,如下例的 gen 就是一個產生器物件。

const gen = generator();
console.log(gen); // [object GeneratorFunction]{}

接著,使用 next 方法來執行其中的 yield 陳述句,它會跑到下一個 yield 前停止。執行 next 方法會得到一個物件 {done: Boolean, value: any},其中 value 為 yield 的回傳值,done 表示是否執行完畢,若為 true 則是執行結束了。

如下,one、two、three 得到的物件皆表示尚未結束,並且回傳其值;oneTwoThree 的 done 為 true 則是結束。

const one = gen.next();
const two = gen.next();
const three = gen.next();
const oneTwoThree = gen.next();

console.log(one); // Object { done: false, value: 1 }
console.log(two); // Object { done: false, value: 2 }
console.log(three); // Object { done: false, value: 3 }
console.log(oneTwoThree); // Object { done: true, value: 123 }

點此 看 demo。

似曾相識?迭代器的寫法如下,只是產生器似乎來得更精簡。

const range = {
  start: 1,
  end: 3,
  [Symbol.iterator]() {
    this.current = this.start;
    return this;
  },
  next() {
    if (this.current >= 1 && this.current <= 3) {
      return { done: false, value: this.current++ };
    }

    return { done: true, value: 123 };
  },
};

const iterator = range[Symbol.iterator]();
const one = iterator.next(); // {done: false, value: 1}
const two = iterator.next(); // {done: false, value: 2}
const three = iterator.next(); // {done: false, value: 3}
const oneTwoThree = iterator.next(); // {done: true, value: 123}

產生器是可迭代的

從上面的例子可得知,產生器是可迭代的。

for (const value of generator()) {
  console.log(value); // 依序印出 1, 2, 3
}

console.log(...generator()); // 1 2 3

用產生器改寫迭代器

用產生器來改寫迭代器吧。

由於 range[Symbol.iterator]() 改為回傳一個產生器,因此就不用之前的 next 方法,並能回傳符合規格的物件 {done: Boolean, value: any}

const range = {
  start: 1,
  end: 3,
  *[Symbol.iterator]() {
    // [Symbol.iterator]: function*() 的縮寫
    for (let value = this.start; value <= this.end; value++) {
      yield value;
    }
  },
};
for (const value of range) {
  console.log(value); // 依序印出 1, 2, 3
}

console.log(...range); // 依序印出 1, 2, 3

點此 看 demo。

比較起來,產生器的寫法似乎比迭代器來得更精簡。

yield* 委派

yield* 委派(yield* delegate)是指將迭代的執行交給接在後面的產生器或可迭代的物件。

如下例,產生器物件 gen 每次恢復執行,就會迭代陣列中的一個元素。

const generator = function*() {
  yield* ['apple', 'boy', 'cat'];
};

const gen = generator();

因此,依序得到 value 為 apple、boy、cat。

gen.next(); // {value: "apple", done: false}
gen.next(); // {value: "boy", done: false}
gen.next(); // {value: "cat", done: false}
gen.next(); // {value: undefined, done: true}

來看一個更複雜的例子,我們利用產生器委派迭代任務給另一個產生器,這麼做的好處是不需要額外的變數(也就是記憶體)來暫存中間產物。如下,產生器 generateSequeatialNumbers 根據傳入的 start 與 from 參數,來決定要回傳(yield)的連續數值的範圍;產生器 generatePasswords 會多次將迭代交給另一個產生器 generateSequeatialNumbers。

function* generateSequeatialNumbers(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generatePasswords() {
  // 0~9
  yield* generateSequeatialNumbers(48, 57);

  // A~Z
  yield* generateSequeatialNumbers(65, 90);

  // a~z
  yield* generateSequeatialNumbers(97, 122);
}

因此,當使用 for...of loop 來迭代執行 generatePasswords 所得到的產生器物件時,會依序迭代 generateSequeatialNumbers 三次,每次因傳入參數 start 與 end 的不同而有不同的結果。

let str = '';

for (const code of generatePasswords()) {
  str += String.fromCharCode(code);
}

console.log(str); // "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

String.fromCharCode 會將傳入的 Unicode 轉為對應的字串。

點此 看 demo。

使用 next/yield 來改變產生器的結果

每一次由 next 開始執行,直到遇到 yield 而暫停。若 next 沒有傳入值,yield 就將其後的運算結果回傳出去;若 next 有傳入值,則捨棄先前 yield 的計算結果,由 next 傳入的值取代。由此可知,yield 幾乎等於賦值(assign,=)的功用。

這裡有一個產生器 foo,內含一陣列,陣列的元素由 yield 運算後產生,以下用 step 來表示進行的順序。

function* foo() {
  const arr = [yield 1, yield 2, yield 3];
  console.log(arr, yield 4); // step 5: console.log(...) 得到 [undefined, undefined, undefined] undefined
}

const it = foo();

console.log(it.next().value); // step 1: 得到 1
console.log(it.next().value); // step 2: 得到 2
console.log(it.next().value); // step 3: 得到 3
console.log(it.next().value); // step 4: 得到 4
console.log(it.next().done); // step 6: 得到 true

執行結果

1
2
3
4
[undefined, undefined, undefined] undefined
true

點此 看 demo。

若 next 有傳入值,會怎麼樣呢?就捨棄先前 yield 後方計算的結果,由下一次 next 傳入的值取代,因此,下方的執行結果一樣都會得到 1、2、3、4,但陣列 array 的內容被傳入的取代了。

function* foo() {
  const arr = [yield 1, yield 2, yield 3];
  console.log(arr, yield 4); // step 5: console.log(...) 得到 [200, 300, 400] 500
}

const it = foo();

console.log(it.next(100).value); // step 1: 得到 1
console.log(it.next(200).value); // step 2: 得到 2,並用 200 取代 yield 1 的結果
console.log(it.next(300).value); // step 3: 得到 3,並用 300 取代 yield 2 的結果
console.log(it.next(400).value); // step 4: 得到 4,並用 400 取代 yield 3 的結果
console.log(it.next(500).done); // step 6: 得到 true,並用 500 取代 yield 4 的結果

執行結果

1
2
3
4
[200, 300, 400] 500
true

點此 看 demo。

拋出錯誤

若想在產生器內拋出錯誤,使用 generator.throw(err) 即可。

function* generator() {
  try {
    const result = yield 'Hello World'; // (3)
    console.log('The execution does not reach here, because the exception is thrown above'); // (4)
  } catch (e) {
    console.log(e.message); // (5) 顯示錯誤訊息
  }
}

const gen = generator();
const result = gen.next().value; // (1)
console.log(result); // Hello World
gen.throw(new Error('Here is an error :->')); // (2)

點此 看 demo。

說明

若沒有 try...catch 區塊來做錯誤捕捉,則程式碼會出錯 Uncaught Error: Here is an error :->

function* generator() {
  const result = yield 'Hello World';
}

const gen = generator();
const result = gen.next().value;
gen.throw(new Error('Here is an error :->'));

得到

Uncaught Error: Here is an error :->

點此 看 demo。

總整理

參考資料


comments powered by Disqus