JavaScript 生成器简介 同步生成器 异步生成器

2025-06-04

JavaScript 生成器简介

同步发电机

异步生成器

立即在http://jauyeung.net/subscribe/订阅我的电子邮件列表

在 Twitter 上关注我:https://twitter.com/AuMayeung

更多文章请访问https://medium.com/@hohanga

更多文章请访问http://thewebdev.info/

在 JavaScript 中,生成器是一种返回生成器对象的特殊函数。该生成器对象包含next一个可迭代对象的值。它允许我们在for...of循环中使用生成器函数来迭代对象集合。这意味着返回的生成器函数遵循可迭代协议。

同步发电机

任何遵循可迭代协议的东西都可以通过for...of循环进行迭代。类似Array或 的对象Map都遵循此协议。生成器函数也遵循迭代器协议。这意味着它以标准方式生成值序列。它实现了next返回至少具有 2 个属性的对象(done和)的函数valuedone属性是一个布尔值,当true迭代器超出迭代序列的末尾时返回。如果它能够生成序列中的下一个项目,那么它就是falsevalue是迭代器返回的项目。如果done是,truevalue可以省略。该next方法始终返回具有上述 2 个属性的对象。如果返回非对象值,TypeError则会抛出 。

要编写生成器函数,我们使用以下代码:

function* strGen() { 
  yield 'a';
  yield 'b';
  yield 'c';
}

const g = strGen();
for (let letter of g){
  console.log(letter)
}

function 关键字后的星号表示该函数是生成器函数。生成器函数只会返回生成器对象。使用生成器函数,函数next会自动生成。生成器还具有一个return函数用于返回给定值并结束生成器,以及一个throw函数用于抛出错误并结束生成器,除非生成器内部捕获到错误。要从生成器返回下一个值,我们使用yield关键字。每次yield调用语句时,生成器都会暂停,直到next再次请求该值。

当执行上述示例时,我们会记录“a”、“b”和“c”,因为生成器是在循环中运行的for...of。每次运行时,都会yield调用下一个语句,并返回语句列表中的下一个值yield

我们还可以编写一个生成器来生成无限的值。我们可以在生成器内部编写一个无限循环来不断返回新值。由于该yield语句直到下一个值被请求时才会运行,因此我们可以保持无限循环运行而不会导致浏览器崩溃。例如,我们可以这样写:

function* genNum() {
  let index = 0;
  while(true){
    yield index += 2;
  }
}
const gen = genNum();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);

如您所见,我们可以使用循环来重复运行yield。该yield语句必须在代码的顶层运行。这意味着它们不能嵌套在另一个回调函数中。该next函数会自动包含在生成器对象中,该生成器对象会被 yield 以从生成器中获取下一个值。

return方法在迭代器结束时调用。也就是说,当获取到最后一个值或该方法抛出错误时thrown。如果我们不想迭代器结束,可以将yield语句包装在子句中,try...finally就像下面的代码一样:

function* genFn() {
  try {
    yield;
  } finally {
    yield 'Keep running';
  }
}

在运行生成器时调用该throw方法,错误将停止生成器,除非在生成器函数中捕获该错误。为了捕获、抛出并捕获错误,我们可以编写类似以下代码:

function* genFn() {
  try {
    console.log('Start');
    yield; // (A)
  } catch (error) {
    console.log(`Caught: ${error}`);
  }
}
const g = genFn();
g.next();
g.throw(new Error('Error'))

如您所见,如果我们运行上面的代码,我们可以看到在运行第一行时会打印“Start”,因为我们只是从生成器对象(也就是该g.next()行)获取第一个值。然后g.throw(new Error('Error'))运行该行会抛出一个错误,该错误会打印在catch子句中。

使用生成器函数,我们还可以使用关键字在其中调用其他生成器函数yield*。以下示例不起作用:

function* genFn() {
  yield 'a'
}
function* genFnToCallgenFn() {
  while (true) {
    yield genFn();
  }
}
const g = genFnToCallgenFn()
console.log(g.next())
console.log(g.next())

如您所见,如果我们运行上面的代码,valuelogged 的​​属性就是生成器函数,这不是我们想要的。这是因为yield关键字没有直接从其他生成器中检索值。这正是yield*关键字有用的地方。如果我们yield genFn();用替换yield* genFn();,那么将检索由 生成的生成器返回的值genFn。在这种情况下,它将继续获取字符串“a”。例如,如果我们改为运行以下代码:

function* genFn() {
  yield 'a'
}
function* genFnToCallgenFn() {
  while (true) {
    yield* genFn();
  }
}
const g = genFnToCallgenFn()
console.log(g.next())
console.log(g.next())

我们将看到,value两个记录的对象中的属性都value设置为“a”。

使用生成器,我们可以编写一个迭代方法,轻松地递归遍历一棵树。例如,我们可以编写以下内容:

class Tree{
  constructor(value, left=null, center=null, right=null) {
    this.value = value;
    this.left = left;
    this.center = center;
    this.right = right;
  }

  *[Symbol.iterator]() {
    yield this.value;
    if (this.left) {
      yield* this.left;
    }
    if (this.center) {
      yield* this.center;
    }
    if (this.right) {
      yield* this.right;
    }
  }
}

在上面的代码中,我们唯一的方法是一个生成器,它返回树中当前节点的左、中、右节点。请注意,我们使用yield*关键字 而不是 ,yield因为 JavaScript 类是生成器函数,而我们的类是一个生成器函数,因为我们有一个用 符号表示的特殊函数Symbol.iterator,这意味着该类将创建一个生成器。

Symbol 是 ES2015 中的新特性。它是一个唯一且不可变的标识符。一旦创建,就无法复制。每次创建一个新的 Symbol 时,它都是唯一的。它主要用于对象中的唯一标识符。这也是 Symbol 的唯一用途。

它有一些自身的静态属性和方法,用于公开全局符号注册表。它就像一个内置对象,但是它没有构造函数,因此我们无法使用关键字new Symbol构造一个 Symbol 对象new

Symbol.iterator是一个特殊符号,表示该函数是一个迭代器。它内置于 JavaScript 标准库中。

如果我们有以下定义代码,那么我们就可以构建树形数据结构:

const tree = new Tree('a',
  new Tree('b',
    new Tree('c'),
    new Tree('d'),
    new Tree('e')
  ),
  new Tree('f'),
  new Tree('g',
    new Tree('h'),
    new Tree('i'),
    new Tree('j')
  )
);

然后我们运行:

for (const str of tree) {  
    console.log(str);  
}

我们按照定义树的顺序记录了树的所有值。定义递归数据结构比不使用生成器函数要容易得多。

我们可以在一个生成器函数中混合使用yieldyield*。例如,我们可以这样写:

function* genFn() {
  yield 'a'
}
function* genFnToCallgenFn() {
  yield 'Start';
  while (true) {
    yield* genFn();
  }
}
const g = genFnToCallgenFn()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())

如果我们运行上面的代码,我们会得到value返回的第一个项目的属性为“Start” g.next()。然后,记录的其他项目都具有“a”作为value属性。

我们还可以使用return语句返回迭代器中想要返回的最后一个值。它的作用yield与生成器函数中的最后一条语句完全相同。例如,我们可以这样写:

function* genFn() {
  yield 'a';
  return 'result';
}
const g = genFn()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())

如果我们查看控制台日志,我们可以看到我们记录的前两行在属性中返回了“a” value,并value在前两console.log行的属性中返回了“result”。然后剩下的一行将设置undefinedvalue。第一个console.log设置donefalse,而其余的done设置为true。这是因为该return语句结束了生成器函数的执行。它下面的任何内容都像常规return语句一样无法访问。

异步生成器

生成器也可用于异步代码。为了创建用于异步代码的生成器函数,我们可以创建一个对象,其中包含一个用特殊符号Symbol.asyncIteratorfunction 表示的方法。例如,我们可以编写以下代码来循环遍历一系列数字,每次迭代间隔 1 秒:

const rangeGen = (from = 1, to = 5) => {
  return {
    from,
    to,
    [Symbol.asyncIterator]() {
      return {
        currentNum: this.from,
        lastNum: this.to,
        async next() {
          await new Promise(resolve => setTimeout(
            resolve, 1000));
          if (this.currentNum <= this.lastNum) {
            return {
              done: false,
              value: this.currentNum++
            };
          } else {
            return {
              done: true
            };
          }
        }
      };
    }
  };
}

(async () => {
  for await (let value of rangeGen()) {
    console.log(value);
  }
})()

请注意,promise 解析后的值在return语句中。next函数应始终返回一个 Promise。我们可以使用循环来迭代生成器生成的值,循环也适用于迭代异步代码。这非常有用,因为我们可以像循环同步代码一样循环执行异步代码,而这在异步生成器函数以及和语法for await...of出现之前是无法实现的。我们返回一个具有属性的对象,就像同步生成器一样。asyncawaitdonevalue

我们可以通过以下方式缩短上述代码:

async function* rangeGen(start = 1, end = 5) {
  for (let i = start; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield i;
  }
}
(async () => {
  for await (let value of rangeGen(1, 10)) {
    console.log(value);
  }
})()

注意,我们可以将yield运算符与async和 一起使用await。 的末尾仍然会返回一个 Promise rangeGen,但这是一种更简洁的方法。它的作用与之前的代码完全相同,但更简洁,也更易于阅读。

生成器函数对于创建可与循环一起使用的迭代器非常有用for...of。该yield语句将从您选择的任何来源获取迭代器返回的下一个值。这意味着我们可以将任何东西变成可迭代的对象。此外,我们可以使用它来迭代树结构,方法是定义一个类,该类具有由 Symbol 表示的方法Symbol.iterator,该方法创建一个生成器函数,该函数使用关键字获取下一级的项目yield*,该关键字直接从生成器函数获取一个项目。此外,我们还有return语句来返回生成器函数中的最后一项。对于异步代码,我们有AsyncIterators,我们可以通过使用asyncawait和来定义它yield,就像我们上面所做的那样按顺序解决承诺。

文章来源:https://dev.to/aumayeung/easy-introduction-to-javascript-generators-5cai
PREV
如何使用 React 制作日历应用
NEXT
🛠️ 📦 TypeScript 泛型 - 速查表