2022 年 8 月 30 日

生成器

常规函数仅返回一个单一值(或无)。

生成器可以按需返回(“生成”)多个值,一次一个。它们与 可迭代对象 配合得很好,可以轻松创建数据流。

生成器函数

要创建生成器,我们需要一个特殊的语法结构:function*,即所谓的“生成器函数”。

它看起来像这样

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

生成器函数的行为与常规函数不同。当调用此类函数时,它不会运行其代码。相反,它返回一个称为“生成器对象”的特殊对象来管理执行。

请看这里

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

// "generator function" creates "generator object"
let generator = generateSequence();
alert(generator); // [object Generator]

函数代码执行尚未开始

生成器的主要方法是 next()。调用时,它会运行执行,直到最近的 yield <value> 语句(可以省略 value,然后为 undefined)。然后函数执行暂停,并且生成的 value 返回到外部代码。

next() 的结果始终是一个具有两个属性的对象

  • value:生成的值。
  • done:如果函数代码已完成,则为 true,否则为 false

例如,这里我们创建生成器并获取其第一个生成值

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

let generator = generateSequence();

let one = generator.next();

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

到目前为止,我们只获取了第一个值,并且函数执行在第二行

我们再次调用 generator.next()。它恢复代码执行并返回下一个 yield

let two = generator.next();

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

并且,如果我们第三次调用它,则执行将到达完成函数的 return 语句

let three = generator.next();

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

现在生成器已完成。我们应该从 done:true 中看到它,并将 value:3 作为最终结果进行处理。

generator.next() 的新调用不再有意义。如果我们这样做,它们将返回相同对象:{done: true}

function* f(…)function *f(…)

两种语法都是正确的。

但通常首选第一种语法,因为星号 * 表示它是一个生成器函数,它描述的是类型,而不是名称,所以它应该与 function 关键字保持一致。

生成器是可迭代的

正如你可能已经从 next() 方法中猜到的那样,生成器是 可迭代的

我们可以使用 for..of 循环访问它们的值

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

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, then 2
}

看起来比调用 .next().value 好多了,对吧?

…但请注意:上面的示例显示了 1,然后是 2,仅此而已。它没有显示 3

这是因为当 done: true 时,for..of 迭代会忽略最后一个 value。因此,如果我们希望 for..of 显示所有结果,我们必须使用 yield 返回它们

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

let generator = generateSequence();

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

由于生成器是可迭代的,我们可以调用所有相关功能,例如展开语法 ...

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

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

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

在上面的代码中,...generateSequence() 将可迭代生成器对象转换为一个项目数组(在章节 剩余参数和展开语法 中详细了解展开语法)

将生成器用于可迭代对象

前段时间,在章节 可迭代对象 中,我们创建了一个可迭代 range 对象,它返回 from..to 的值。

这里,让我们回顾一下代码

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

  // for..of range calls this method once in the very beginning
  [Symbol.iterator]() {
    // ...it returns the iterator object:
    // onward, for..of works only with that object, asking it for next values
    return {
      current: this.from,
      last: this.to,

      // next() is called on each iteration by the for..of loop
      next() {
        // it should return the value as an object {done:.., value :...}
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

// iteration over range returns numbers from range.from to range.to
alert([...range]); // 1,2,3,4,5

我们可以通过将生成器函数作为 Symbol.iterator 提供,来使用它进行迭代。

这里有相同的 range,但更加简洁

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

  *[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

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

这是可行的,因为 range[Symbol.iterator]() 现在返回一个生成器,而生成器方法正是 for..of 所期望的

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

当然,这不是巧合。生成器被添加到 JavaScript 语言中,是为了实现迭代器,以便轻松地实现它们。

带有生成器的变体比 range 的原始可迭代代码简洁得多,并且保留了相同的功能。

生成器可以永远生成值

在上面的示例中,我们生成了有限序列,但我们还可以创建一个永远生成值的生成器。例如,一个伪随机数的无穷序列。

对于这样的生成器,在 for..of 中肯定需要一个 break(或 return)。否则,循环将永远重复并挂起。

生成器组合

生成器组合是生成器的特殊功能,它允许在彼此中透明地“嵌入”生成器。

例如,我们有一个生成数字序列的函数

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

现在,我们希望重复使用它来生成更复杂的序列

  • 首先,数字 0..9(字符代码 48…57),
  • 其次,大写字母 A..Z(字符代码 65…90)
  • 其次,小写字母 a..z(字符代码 97…122)

我们可以使用此序列,例如通过从中选择字符来创建密码(还可以添加语法字符),但让我们首先生成它。

在常规函数中,要组合来自多个其他函数的结果,我们调用它们,存储结果,然后在最后连接。

对于生成器,有一个特殊的 yield* 语法来“嵌入”(组合)一个生成器到另一个生成器中。

组合生成器

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* 指令将执行委托给另一个生成器。此术语表示 yield* gen 迭代生成器 gen,并透明地将其外部的 yield 转发。就好像这些值是由外部生成器生成的。

结果与我们内联嵌套生成器中的代码相同

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

生成器组合是将一个生成器的流插入另一个生成器的自然方式。它不使用额外的内存来存储中间结果。

“yield” 是双向的

到目前为止,生成器与可迭代对象类似,具有生成值的特殊语法。但实际上它们更强大、更灵活。

这是因为 yield 是双向的:它不仅将结果返回到外部,还可以将值传递到生成器内部。

要执行此操作,我们应该使用参数调用 generator.next(arg)。该参数成为 yield 的结果。

我们来看一个示例

function* gen() {
  // Pass a question to the outer code and wait for an answer
  let result = yield "2 + 2 = ?"; // (*)

  alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield returns the value

generator.next(4); // --> pass the result into the generator
  1. 第一个调用 generator.next() 应始终在没有参数的情况下进行(如果传递参数,则忽略该参数)。它启动执行并返回第一个 yield "2+2=?" 的结果。此时,生成器暂停执行,同时停留在行 (*) 上。
  2. 然后,如上图所示,yield 的结果进入调用代码中的 question 变量。
  3. generator.next(4) 上,生成器恢复,并且 4 作为结果进入:let result = 4

请注意,外部代码不必立即调用 next(4)。可能需要时间。这没问题:生成器将等待。

例如

// resume the generator after some time
setTimeout(() => generator.next(4), 1000);

正如我们所看到的,与常规函数不同,生成器和调用代码可以通过在 next/yield 中传递值来交换结果。

为了让事情更明显,这里提供另一个示例,其中包含更多调用

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

执行图片

  1. 第一个 .next() 启动执行…它到达第一个 yield
  2. 结果返回到外部代码。
  3. 第二个 .next(4)4 作为第一个 yield 的结果传递回生成器,并恢复执行。
  4. …它到达第二个 yield,该 yield 成为生成器调用的结果。
  5. 第三个 next(9)9 作为第二个 yield 的结果传递到生成器中,并恢复到达函数末尾的执行,因此 done: true

这就像一个“乒乓”游戏。每个 next(value)(第一个除外)将一个值传递到生成器中,该值成为当前 yield 的结果,然后获取下一个 yield 的结果。

generator.throw

正如我们在上面的示例中观察到的,外部代码可以将值传递到生成器中,作为 `yield` 的结果。

…但它也可以在那里引发(抛出)错误。这是很自然的,因为错误是一种结果。

要将错误传递到 `yield` 中,我们应该调用 `generator.throw(err)`。在这种情况下,`err` 将在带有该 `yield` 的行中抛出。

例如,这里 `“2 + 2 = ?”` 的 yield 导致错误

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); // shows the error
  }
}

let generator = gen();

let question = generator.next().value;

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

在第 `(2)` 行抛入生成器的错误导致第 `(1)` 行中带有 `yield` 的异常。在上面的示例中,`try..catch` 捕获并显示它。

如果我们不捕获它,那么就像任何异常一样,它会“从”生成器“掉入”调用代码中。

调用代码的当前行是带有 `generator.throw` 的行,标记为 `(2)`。因此,我们可以像这样在这里捕获它

function* generate() {
  let result = yield "2 + 2 = ?"; // Error in this line
}

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); // shows the error
}

如果我们不捕获那里的错误,那么它通常会像往常一样传递到外部调用代码(如果有),并且如果未捕获,则会终止脚本。

generator.return

generator.return(value) 完成生成器执行并返回给定的 `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 }

如果我们再次在已完成的生成器中使用 `generator.return()`,它将再次返回该值 (MDN)。

我们通常不使用它,因为大多数情况下我们希望获取所有返回值,但当我们希望在特定条件下停止生成器时,它可能很有用。

总结

  • 生成器由生成器函数 `function* f(…) {…}` 创建。
  • 仅在生成器内部存在 `yield` 运算符。
  • 外部代码和生成器可以通过 `next/yield` 调用交换结果。

在现代 JavaScript 中,生成器很少使用。但有时它们很方便,因为函数在执行期间与调用代码交换数据的能力是相当独特的。而且,当然,它们非常适合制作可迭代对象。

此外,在下一章中,我们将学习异步生成器,它用于在 `for await ... of` 循环中读取异步生成的数据流(例如通过网络进行分页获取)。

在 Web 编程中,我们经常使用流式数据,所以这是另一个非常重要的用例。

任务

我们需要随机数据的地方有很多。

其中之一是测试。我们可能需要随机数据:文本、数字等来很好地测试事物。

在 JavaScript 中,我们可以使用 `Math.random()`。但如果出现问题,我们希望能够重复测试,使用完全相同的数据。

为此,使用了所谓的“种子伪随机生成器”。它们获取一个“种子”,即第一个值,然后使用公式生成后续值,以便相同的种子产生相同的序列,因此整个流程很容易重现。我们只需要记住种子即可重复它。

生成分布较为均匀的值的此类公式示例

next = previous * 16807 % 2147483647

如果我们使用1作为种子,则值将为

  1. 16807
  2. 282475249
  3. 1622650073
  4. …依此类推…

任务是创建一个生成器函数pseudoRandom(seed),它获取seed并使用此公式创建生成器。

使用示例

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

请注意,可以使用常规函数执行相同操作,如下所示

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进行迭代和使用生成器组合的能力,这在其他地方可能很有用。

在沙盒中使用测试打开解决方案。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议 - 请提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入几行代码,请使用<code>标签,对于多行 - 将它们包装在<pre>标签中,对于 10 行以上 - 使用沙盒 (plnkrjsbincodepen…)