Javascript generator

2023-02-17 10:53 更新

常規(guī)函數(shù)只會(huì)返回一個(gè)單一值(或者不返回任何值)。

而 generator 可以按需一個(gè)接一個(gè)地返回(“yield”)多個(gè)值。它們可與 iterable 完美配合使用,從而可以輕松地創(chuàng)建數(shù)據(jù)流。

generator 函數(shù)

要?jiǎng)?chuàng)建一個(gè) generator,我們需要一個(gè)特殊的語(yǔ)法結(jié)構(gòu):function*,即所謂的 “generator function”。

它看起來(lái)像這樣:

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

generator 函數(shù)與常規(guī)函數(shù)的行為不同。在此類函數(shù)被調(diào)用時(shí),它不會(huì)運(yùn)行其代碼。而是返回一個(gè)被稱為 “generator object” 的特殊對(duì)象,來(lái)管理執(zhí)行流程。

我們來(lái)看一個(gè)例子:

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

// "generator function" 創(chuàng)建了一個(gè) "generator object"
let generator = generateSequence();
alert(generator); // [object Generator]

到目前為止,上面這段代碼中的 函數(shù)體 代碼還沒有開始執(zhí)行:


一個(gè) generator 的主要方法就是 next()。當(dāng)被調(diào)用時(shí)(譯注:指 next() 方法),它會(huì)恢復(fù)上圖所示的運(yùn)行,執(zhí)行直到最近的 yield <value> 語(yǔ)句(value 可以被省略,默認(rèn)為 undefined)。然后函數(shù)執(zhí)行暫停,并將產(chǎn)出的(yielded)值返回到外部代碼。

next() 的結(jié)果始終是一個(gè)具有兩個(gè)屬性的對(duì)象:

  • ?value?: 產(chǎn)出的(yielded)的值。
  • ?done?: 如果 generator 函數(shù)已執(zhí)行完成則為 ?true?,否則為 ?false?。

例如,我們可以創(chuàng)建一個(gè) generator 并獲取其第一個(gè)產(chǎn)出的(yielded)值:

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

let generator = generateSequence();

let one = generator.next();

alert(JSON.stringify(one)); // {value: 1, done: false}

截至目前,我們只獲得了第一個(gè)值,現(xiàn)在函數(shù)執(zhí)行處在第二行:


讓我們?cè)俅握{(diào)用 generator.next()。代碼恢復(fù)執(zhí)行并返回下一個(gè) yield 的值:

let two = generator.next();

alert(JSON.stringify(two)); // {value: 2, done: false}


如果我們第三次調(diào)用 generator.next(),代碼將會(huì)執(zhí)行到 return 語(yǔ)句,此時(shí)就完成這個(gè)函數(shù)的執(zhí)行:

let three = generator.next();

alert(JSON.stringify(three)); // {value: 3, done: true}


現(xiàn)在 generator 執(zhí)行完成。我們通過 done:true 可以看出來(lái)這一點(diǎn),并且將 value:3 處理為最終結(jié)果。

再對(duì) generator.next() 進(jìn)行新的調(diào)用不再有任何意義。如果我們這樣做,它將返回相同的對(duì)象:{done: true}

?function* f(…)? 或 ?function *f(…)??

這兩種語(yǔ)法都是對(duì)的。

但是通常更傾向于第一種語(yǔ)法,因?yàn)樾翘?hào) * 表示它是一個(gè) generator 函數(shù),它描述的是函數(shù)種類而不是名稱,因此 * 應(yīng)該和 function 關(guān)鍵字緊貼一起。

generator 是可迭代的

當(dāng)你看到 next() 方法,或許你已經(jīng)猜到了 generator 是 可迭代(iterable)的。

我們可以使用 for..of 循環(huán)遍歷它所有的值:

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

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1,然后是 2
}

for..of 寫法是不是看起來(lái)比 .next().value 優(yōu)雅多了?

……但是請(qǐng)注意:上面這個(gè)例子會(huì)先顯示 1,然后是 2,然后就沒了。它不會(huì)顯示 3!

這是因?yàn)楫?dāng) done: true 時(shí),for..of 循環(huán)會(huì)忽略最后一個(gè) value。因此,如果我們想要通過 for..of 循環(huán)顯示所有的結(jié)果,我們必須使用 yield 返回它們:

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

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1,然后是 2,然后是 3
}

因?yàn)?generator 是可迭代的,我們可以使用 iterator 的所有相關(guān)功能,例如:spread 語(yǔ)法 ...

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

let sequence = [0, ...generateSequence()];

alert(sequence); // 0, 1, 2, 3

在上面這段代碼中,...generateSequence() 將可迭代的 generator 對(duì)象轉(zhuǎn)換為了一個(gè)數(shù)組(關(guān)于 spread 語(yǔ)法的更多細(xì)節(jié)請(qǐng)見 Rest 參數(shù)與 Spread 語(yǔ)法)。

使用 generator 進(jìn)行迭代

在前面的 Iterable object(可迭代對(duì)象) 一章中,我們創(chuàng)建了一個(gè)可迭代的 range 對(duì)象,它返回 from..to 的值。

現(xiàn)在,我們回憶一下代碼:

let range = {
  from: 1,
  to: 5,

  // for..of range 在一開始就調(diào)用一次這個(gè)方法
  [Symbol.iterator]() {
    // ...它返回 iterator object:
    // 后續(xù)的操作中,for..of 將只針對(duì)這個(gè)對(duì)象,并使用 next() 向它請(qǐng)求下一個(gè)值
    return {
      current: this.from,
      last: this.to,

      // for..of 循環(huán)在每次迭代時(shí)都會(huì)調(diào)用 next()
      next() {
        // 它應(yīng)該以對(duì)象 {done:.., value :...} 的形式返回值
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

// 迭代整個(gè) range 對(duì)象,返回從 `range.from` 到 `range.to` 范圍的所有數(shù)字
alert([...range]); // 1,2,3,4,5

我們可以通過提供一個(gè) generator 函數(shù)作為 Symbol.iterator,來(lái)使用 generator 進(jìn)行迭代:

下面是一個(gè)相同的 range,但緊湊得多:

let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() { // [Symbol.iterator]: function*() 的簡(jiǎn)寫形式
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

alert( [...range] ); // 1,2,3,4,5

之所以代碼正常工作,是因?yàn)?nbsp;range[Symbol.iterator]() 現(xiàn)在返回一個(gè) generator,而 generator 方法正是 for..of 所期望的:

  • 它具有 ?.next()? 方法
  • 它以 ?{value: ..., done: true/false}? 的形式返回值

當(dāng)然,這不是巧合。generator 被添加到 JavaScript 語(yǔ)言中是有對(duì) iterator 的考量的,以便更容易地實(shí)現(xiàn) iterator。

帶有 generator 的變體比原來(lái)的 range 迭代代碼簡(jiǎn)潔得多,并且保持了相同的功能。

generator 可以永遠(yuǎn)產(chǎn)出(yield)值

在上面的示例中,我們生成了有限序列,但是我們也可以創(chuàng)建一個(gè)生成無(wú)限序列的 generator,它可以一直產(chǎn)出(yield)值。例如,無(wú)序的偽隨機(jī)數(shù)序列。

這種情況下肯定需要在 generator 的 for..of 循環(huán)中添加一個(gè) break(或者 return)。否則循環(huán)將永遠(yuǎn)重復(fù)下去并掛起。

generator 組合

generator 組合(composition)是 generator 的一個(gè)特殊功能,它允許透明地(transparently)將 generator 彼此“嵌入(embed)”到一起。

例如,我們有一個(gè)生成數(shù)字序列的函數(shù):

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

現(xiàn)在,我們想重用它來(lái)生成一個(gè)更復(fù)雜的序列:

  • 首先是數(shù)字 ?0..9?(字符代碼為 48…57),
  • 接下來(lái)是大寫字母 ?A..Z?(字符代碼為 65…90)
  • 接下來(lái)是小寫字母 ?a...z?(字符代碼為 97…122)

我們可以對(duì)這個(gè)序列進(jìn)行應(yīng)用,例如,我們可以從這個(gè)序列中選擇字符來(lái)創(chuàng)建密碼(也可以添加語(yǔ)法字符),但讓我們先生成它。

在常規(guī)函數(shù)中,要合并其他多個(gè)函數(shù)的結(jié)果,我們需要調(diào)用它們,存儲(chǔ)它們的結(jié)果,最后再將它們合并到一起。

對(duì)于 generator 而言,我們可以使用 yield* 這個(gè)特殊的語(yǔ)法來(lái)將一個(gè) generator “嵌入”(組合)到另一個(gè) generator 中:

組合的 generator 的例子:

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

function* generatePasswordCodes() {

  // 0..9
  yield* generateSequence(48, 57);

  // A..Z
  yield* generateSequence(65, 90);

  // a..z
  yield* generateSequence(97, 122);

}

let str = '';

for(let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

yield* 指令將執(zhí)行 委托 給另一個(gè) generator。這個(gè)術(shù)語(yǔ)意味著 yield* gen 在 generator gen 上進(jìn)行迭代,并將其產(chǎn)出(yield)的值透明地(transparently)轉(zhuǎn)發(fā)到外部。就好像這些值就是由外部的 generator yield 的一樣。

執(zhí)行結(jié)果與我們內(nèi)聯(lián)嵌套 generator 中的代碼獲得的結(jié)果相同:

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

function* generateAlphaNum() {

  // yield* generateSequence(48, 57);
  for (let i = 48; i <= 57; i++) yield i;

  // yield* generateSequence(65, 90);
  for (let i = 65; i <= 90; i++) yield i;

  // yield* generateSequence(97, 122);
  for (let i = 97; i <= 122; i++) yield i;

}

let str = '';

for(let code of generateAlphaNum()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

generator 組合(composition)是將一個(gè) generator 流插入到另一個(gè) generator 流的自然的方式。它不需要使用額外的內(nèi)存來(lái)存儲(chǔ)中間結(jié)果。

“yield” 是一條雙向路

目前看來(lái),generator 和可迭代對(duì)象類似,都具有用來(lái)生成值的特殊語(yǔ)法。但實(shí)際上,generator 更加強(qiáng)大且靈活。

這是因?yàn)?nbsp;yield 是一條雙向路(two-way street):它不僅可以向外返回結(jié)果,而且還可以將外部的值傳遞到 generator 內(nèi)。

調(diào)用 generator.next(arg),我們就能將參數(shù) arg 傳遞到 generator 內(nèi)部。這個(gè) arg 參數(shù)會(huì)變成 yield 的結(jié)果。

我們來(lái)看一個(gè)例子:

function* gen() {
  // 向外部代碼傳遞一個(gè)問題并等待答案
  let result = yield "2 + 2 = ?"; // (*)

  alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield 返回的 value

generator.next(4); // --> 將結(jié)果傳遞到 generator 中


  1. 第一次調(diào)用 ?generator.next()? 應(yīng)該是不帶參數(shù)的(如果帶參數(shù),那么該參數(shù)會(huì)被忽略)。它開始執(zhí)行并返回第一個(gè) ?yield "2 + 2 = ?"? 的結(jié)果。此時(shí),generator 執(zhí)行暫停,而停留在 (?*?) 行上。
  2. 然后,正如上面圖片中顯示的那樣,?yield? 的結(jié)果進(jìn)入調(diào)用代碼中的 ?question? 變量。
  3. 在 ?generator.next(4)?,generator 恢復(fù)執(zhí)行,并獲得了 ?4? 作為結(jié)果:?let result = 4?。

請(qǐng)注意,外部代碼不必立即調(diào)用 next(4)。外部代碼可能需要一些時(shí)間。這沒問題:generator 將等待它。

例如:

// 一段時(shí)間后恢復(fù) generator
setTimeout(() => generator.next(4), 1000);

我們可以看到,與常規(guī)函數(shù)不同,generator 和調(diào)用 generator 的代碼可以通過在 next/yield 中傳遞值來(lái)交換結(jié)果。

為了講得更淺顯易懂,我們來(lái)看另一個(gè)例子,其中包含了許多調(diào)用:

function* gen() {
  let ask1 = yield "2 + 2 = ?";

  alert(ask1); // 4

  let ask2 = yield "3 * 3 = ?"

  alert(ask2); // 9
}

let generator = gen();

alert( generator.next().value ); // "2 + 2 = ?"

alert( generator.next(4).value ); // "3 * 3 = ?"

alert( generator.next(9).done ); // true

執(zhí)行圖:


  1. 第一個(gè) ?.next()? 啟動(dòng)了 generator 的執(zhí)行……執(zhí)行到達(dá)第一個(gè) ?yield?。
  2. 結(jié)果被返回到外部代碼中。
  3. 第二個(gè) ?.next(4)? 將 4 作為第一個(gè) ?yield? 的結(jié)果傳遞回 generator 并恢復(fù) generator 的執(zhí)行。
  4. ……執(zhí)行到達(dá)第二個(gè) ?yield?,它變成了 generator 調(diào)用的結(jié)果。
  5. 第三個(gè) ?next(9)? 將 ?9? 作為第二個(gè) ?yield? 的結(jié)果傳入 generator 并恢復(fù) generator 的執(zhí)行,執(zhí)行現(xiàn)在到達(dá)了函數(shù)的最底部,所以返回 ?done: true?。

這個(gè)過程就像“乒乓球”游戲。每個(gè) next(value)(除了第一個(gè))傳遞一個(gè)值到 generator 中,該值變成了當(dāng)前 yield 的結(jié)果,然后獲取下一個(gè) yield 的結(jié)果。

generator.throw

正如我們?cè)谏厦娴睦又杏^察到的那樣,外部代碼可能會(huì)將一個(gè)值傳遞到 generator,作為 yield 的結(jié)果。

……但是它也可以在那里發(fā)起(拋出)一個(gè) error。這很自然,因?yàn)?error 本身也是一種結(jié)果。

要向 yield 傳遞一個(gè) error,我們應(yīng)該調(diào)用 generator.throw(err)。在這種情況下,err 將被拋到對(duì)應(yīng)的 yield 所在的那一行。

例如,"2 + 2?" 的 yield 導(dǎo)致了一個(gè) error:

function* gen() {
  try {
    let result = yield "2 + 2 = ?"; // (1)

    alert("The execution does not reach here, because the exception is thrown above");
  } catch(e) {
    alert(e); // 顯示這個(gè) error
  }
}

let generator = gen();

let question = generator.next().value;

generator.throw(new Error("The answer is not found in my database")); // (2)

在 (2) 行引入到 generator 的 error 導(dǎo)致了在 (1) 行中的 yield 出現(xiàn)了一個(gè)異常。在上面這個(gè)例子中,try..catch 捕獲并顯示了這個(gè) error。

如果我們沒有捕獲它,那么就會(huì)像其他的異常一樣,它將從 generator “掉出”到調(diào)用代碼中。

調(diào)用代碼的當(dāng)前行是 generator.throw 所在的那一行,標(biāo)記為 (2)。所以我們可以在這里捕獲它,就像這樣:

function* generate() {
  let result = yield "2 + 2 = ?"; // 這行出現(xiàn) error
}

let generator = generate();

let question = generator.next().value;

try {
  generator.throw(new Error("The answer is not found in my database"));
} catch(e) {
  alert(e); // 顯示這個(gè) error
}

如果我們沒有在那里捕獲這個(gè) error,那么,通常,它會(huì)掉入外部調(diào)用代碼(如果有),如果在外部也沒有被捕獲,則會(huì)殺死腳本。

generator.return

generator.return(value) 完成 generator 的執(zhí)行并返回給定的 value。

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

const g = gen();

g.next();        // { value: 1, done: false }
g.return('foo'); // { value: "foo", done: true }
g.next();        // { value: undefined, done: true }

如果我們?cè)谝淹瓿傻?generator 上再次使用 generator.return(),它將再次返回該值(MDN)。

通常我們不使用它,因?yàn)榇蠖鄶?shù)時(shí)候我們想要獲取所有的返回值,但是當(dāng)我們想要在特定條件下停止 generator 時(shí)它會(huì)很有用。

總結(jié)

  • generator 是通過 generator 函數(shù) ?function* f(…) {…}? 創(chuàng)建的。
  • 在 generator(僅在)內(nèi)部,存在 ?yield? 操作。
  • 外部代碼和 generator 可能會(huì)通過 ?next/yield? 調(diào)用交換結(jié)果。

在現(xiàn)代 JavaScript 中,generator 很少被使用。但有時(shí)它們會(huì)派上用場(chǎng),因?yàn)楹瘮?shù)在執(zhí)行過程中與調(diào)用代碼交換數(shù)據(jù)的能力是非常獨(dú)特的。而且,當(dāng)然,它們非常適合創(chuàng)建可迭代對(duì)象。

并且,在下一章我們將會(huì)學(xué)習(xí) async generator,它們被用于在 for await ... of 循環(huán)中讀取異步生成的數(shù)據(jù)流(例如,通過網(wǎng)絡(luò)分頁(yè)提取 (paginated fetches over a network))。

在 Web 編程中,我們經(jīng)常使用數(shù)據(jù)流,因此這是另一個(gè)非常重要的使用場(chǎng)景。

任務(wù)


偽隨機(jī) generator

在很多地方我們都需要隨機(jī)數(shù)據(jù)。

其中之一就是測(cè)試。我們可能需要隨機(jī)數(shù)據(jù):文本,數(shù)字等,以便很好地進(jìn)行測(cè)試。

在 JavaScript 中,我們可以使用 Math.random()。但是如果什么地方出現(xiàn)了問題,我們希望能使用完全相同的數(shù)據(jù)進(jìn)行重復(fù)測(cè)試。

為此,我們可以使用所謂的“種子偽隨機(jī)(seeded pseudo-random)generator”。它們將“種子(seed)”作為第一個(gè)值,然后使用公式生成下一個(gè)值。以便相同的種子(seed)可以產(chǎn)出(yield)相同的序列,因此整個(gè)數(shù)據(jù)流很容易復(fù)現(xiàn)。我們只需要記住種子并重復(fù)它即可。

這樣的公式的一個(gè)示例如下,它可以生成一些均勻分布的值:

next = previous * 16807 % 2147483647

如果我們使用 1 作為種子,生成的值將會(huì)是:

  1. ?16807?
  2. ?282475249?
  3. ?1622650073?
  4. ……等……

這里的任務(wù)是創(chuàng)建一個(gè) generator 函數(shù) pseudoRandom(seed),它將 seed 作為參數(shù)并使用此公式創(chuàng)建 generator。

使用范例:

let generator = pseudoRandom(1);

alert(generator.next().value); // 16807
alert(generator.next().value); // 282475249
alert(generator.next().value); // 1622650073

解決方案

function* pseudoRandom(seed) {
  let value = seed;

  while(true) {
    value = value * 16807 % 2147483647
    yield value;
  }

};

let generator = pseudoRandom(1);

alert(generator.next().value); // 16807
alert(generator.next().value); // 282475249
alert(generator.next().value); // 1622650073

請(qǐng)注意,也可以使用常規(guī)函數(shù)來(lái)完成相同的操作,就像這樣:

function pseudoRandom(seed) {
  let value = seed;

  return function() {
    value = value * 16807 % 2147483647;
    return value;
  }
}

let generator = pseudoRandom(1);

alert(generator()); // 16807
alert(generator()); // 282475249
alert(generator()); // 1622650073

這也可以工作。但是這樣我們就失去了使用 for..of 來(lái)進(jìn)行迭代以及使用 generator 組合(composition)的能力,這些可能在其他地方很有用。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)