迭代器和生成器

迭代器和生成器将迭代概念直接引入核心语言,并提供了一种机制来定制 for...of 循环的行为。

有关详情,另请参阅:

迭代器

在 JavaScript 中,**迭代器**是一个对象,它定义了一个序列,并在其终止时可能返回一个值。

具体来说,迭代器是任何实现 迭代器协议 的对象,它通过一个 next() 方法返回一个具有两个属性的对象:

value

迭代序列中的下一个值。

done

如果序列中的最后一个值已经被消费,则此属性为 true。如果 valuedone 同时存在,则 value 是迭代器的返回值。

一旦创建,迭代器对象可以通过重复调用 next() 来显式迭代。对迭代器进行迭代被称为消费迭代器,因为通常只能进行一次。在产生终止值后,额外调用 next() 应该继续返回 {done: true}

JavaScript 中最常见的迭代器是 Array 迭代器,它按顺序返回关联数组中的每个值。

虽然很容易想象所有迭代器都可以表示为数组,但事实并非如此。数组必须整体分配,但迭代器只在必要时才被消费。因此,迭代器可以表示无限大小的序列,例如 0Infinity 之间的整数范围。

这里有一个可以做到这一点的例子。它允许创建一个范围迭代器,该迭代器定义了一个从 start(包含)到 end(不包含)以 step 间隔的整数序列。其最终返回值是它创建的序列的大小,由变量 iterationCount 跟踪。

js
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;

  const rangeIterator = {
    next() {
      let result;
      if (nextIndex < end) {
        result = { value: nextIndex, done: false };
        nextIndex += step;
        iterationCount++;
        return result;
      }
      return { value: iterationCount, done: true };
    },
  };
  return rangeIterator;
}

然后使用迭代器看起来像这样:

js
const iter = makeRangeIterator(1, 10, 2);

let result = iter.next();
while (!result.done) {
  console.log(result.value); // 1 3 5 7 9
  result = iter.next();
}

console.log("Iterated over sequence of size:", result.value); // [5 numbers returned, that took interval in between: 0 to 10]

注意:无法通过反射知道某个特定对象是否是迭代器。如果您需要这样做,请使用可迭代对象

生成器函数

虽然自定义迭代器是一个有用的工具,但由于需要显式维护其内部状态,它们的创建需要仔细编程。**生成器函数**提供了一个强大的替代方案:它们允许您通过编写一个执行不是连续的函数来定义迭代算法。生成器函数使用 function* 语法编写。

当被调用时,生成器函数最初不会执行其代码。相反,它们返回一种特殊类型的迭代器,称为 **Generator**。当通过调用生成器的 next 方法消费一个值时,Generator 函数会一直执行,直到遇到 yield 关键字。

该函数可以根据需要调用任意次,每次都会返回一个新的 Generator。每个 Generator 只能迭代一次。

我们现在可以修改上面的例子。这段代码的行为是相同的,但实现起来更容易编写和阅读。

js
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let iterationCount = 0;
  for (let i = start; i < end; i += step) {
    iterationCount++;
    yield i;
  }
  return iterationCount;
}

可迭代对象

如果一个对象定义了它的迭代行为,例如在 for...of 构造中循环遍历哪些值,那么它就是**可迭代的**。一些内置类型,例如 ArrayMap,具有默认的迭代行为,而其他类型(例如 Object)则没有。

为了成为**可迭代的**,一个对象必须实现 [Symbol.iterator]() 方法。这意味着该对象(或其原型链上的其中一个对象)必须有一个以 Symbol.iterator 为键的属性。

一个可迭代对象可能可以迭代多次,也可能只能迭代一次。这取决于程序员知道属于哪种情况。

只能迭代一次的可迭代对象(例如生成器)通常从它们的 [Symbol.iterator]() 方法返回 this,而可以迭代多次的可迭代对象必须在每次调用 [Symbol.iterator]() 时返回一个新的迭代器。

js
function* makeIterator() {
  yield 1;
  yield 2;
}

const iter = makeIterator();

for (const itItem of iter) {
  console.log(itItem);
}

console.log(iter[Symbol.iterator]() === iter); // true

// This example show us generator(iterator) is iterable object,
// which has the [Symbol.iterator]() method return the `iter` (itself),
// and consequently, the it object can iterate only _once_.

// If we change the [Symbol.iterator]() method of `iter` to a function/generator
// which returns a new iterator/generator object, `iter`
// can iterate many times

iter[Symbol.iterator] = function* () {
  yield 2;
  yield 1;
};

用户自定义可迭代对象

你可以像这样创建自己的可迭代对象:

js
const myIterable = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  },
};

用户定义的可迭代对象可以在 for...of 循环或扩展语法中照常使用。

js
for (const value of myIterable) {
  console.log(value);
}
// 1
// 2
// 3

[...myIterable]; // [1, 2, 3]

内置可迭代对象

StringArrayTypedArrayMapSet 都是内置可迭代对象,因为它们的原型对象都具有 Symbol.iterator 方法。

期望可迭代对象的语法

一些语句和表达式期望可迭代对象。例如:for...of 循环、扩展语法yield*解构语法。

js
for (const value of ["a", "b", "c"]) {
  console.log(value);
}
// "a"
// "b"
// "c"

[..."abc"];
// ["a", "b", "c"]

function* gen() {
  yield* ["a", "b", "c"];
}

gen().next();
// { value: "a", done: false }

[a, b, c] = new Set(["a", "b", "c"]);
a;
// "a"

高级生成器

生成器**按需**计算其 yield 的值,这使得它们能够高效地表示计算成本高昂的序列(甚至无限序列,如上所示)。

next() 方法还接受一个值,该值可用于修改生成器的内部状态。传递给 next() 的值将被 yield 接收。

注意:传递给 next() 的**首次**调用总是被忽略。

这是使用 next(x) 重新启动序列的斐波那契生成器:

js
function* fibonacci() {
  let current = 0;
  let next = 1;
  while (true) {
    const reset = yield current;
    [current, next] = [next, next + current];
    if (reset) {
      current = 0;
      next = 1;
    }
  }
}

const sequence = fibonacci();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2

您可以通过调用生成器的 throw() 方法并传入它应该抛出的异常值来强制生成器抛出异常。这个异常将从生成器当前挂起的环境中抛出,就像当前挂起的 yield 变成了一个 throw value 语句一样。

如果异常没有在生成器内部捕获,它将通过 throw() 的调用向上传播,并且后续调用 next() 将导致 done 属性为 true

生成器有一个 return() 方法,该方法返回给定值并终止生成器本身。