Skip to content

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

若想了解更多详情,请参考:

GPT介绍

在 JavaScript 中,迭代器是一种特殊对象,它允许你按照一定顺序访问集合中的元素,一个接一个。迭代器是 ES6 引入的一部分,是对迭代模式的一种实现。使用迭代器的主要优点是它提供了一种统一的方法来遍历各种数据结构,无论是数组、字符串还是其他可迭代对象。

迭代器的基本原理

迭代器对象必须实现 next() 方法,该方法返回一个包含两个属性的对象:

  • value:当前遍历的元素的值。
  • done:一个布尔值,如果迭代器已经遍历完集合则为 true,否则为 false

创建自定义迭代器

虽然许多内置类型(如数组、字符串、Map 和 Set)都具有默认的迭代器行为,你也可以创建自定义迭代器来满足特定需求。下面是一个创建简单迭代器的例子,它逐一返回给定数组的元素:

js
function makeIterator(array) {
    let nextIndex = 0;
    return {
        next: function() {
            return nextIndex < array.length ?
                {value: array[nextIndex++], done: false} :
                {done: true};
        }
    };
}

const it = makeIterator(['apple', 'banana', 'cherry']);
console.log(it.next().value); // 输出 'apple'
console.log(it.next().value); // 输出 'banana'
console.log(it.next().value); // 输出 'cherry'
console.log(it.next().done);  // 输出 true

使用 for...of 循环

ES6 引入了 for...of 循环,它可以自动遍历任何可迭代的对象。使用 for...of 循环时,JavaScript 引擎会在后台查找对象的迭代器,并自动处理 next() 方法的调用和 done 状态的检查。

js
const array = ['apple', 'banana', 'cherry'];
for (const fruit of array) {
    console.log(fruit);
}

使用生成器简化迭代器的创建

生成器是一种特殊的函数,可以使用 function* 语法定义,并通过 yield 关键字返回连续的值。生成器自动实现迭代器协议,因此可以非常简单地创建复杂的迭代逻辑:

js
function* idGenerator() {
    let id = 0;
    while (true) {
        yield id++;
    }
}

const it = idGenerator();
console.log(it.next().value); // 输出 0
console.log(it.next().value); // 输出 1
console.log(it.next().value); // 输出 2

迭代器的用途

迭代器非常有用,可以用于:

  • 自定义数据结构的遍历逻辑。
  • 管理异步编程中的同步流程。
  • 实现懒计算,生成器在每次迭代时才计算下一个值,从而节省资源。
  • 处理无限序列,生成器可以产生一个无限序列,但每次只处理序列的一部分。

通过这些例子和说明,你可以看到迭代器和生成器如何强化 JavaScript 的表达力,使得数据处理更为灵活和高效。

迭代器

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

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

value 迭代序列的下一个值。

done 如果已经迭代到序列中的最后一个值,则它为 true。如果 value 和 done 一起出现,则它就是迭代器的返回值。

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

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

虽然很容易想象所有迭代器都可以表示为数组,但事实并非如此。数组必须完整分配,而迭代器则是按需分配。因此,迭代器可以表示无限大小的序列,例如 0 和 Infinity 之间的整数范围。

下面的例子展示了具体做法。它允许你创建一个简单的范围迭代器,以定义一个从 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
let it = makeRangeIterator(1, 10, 2);

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

console.log(`已迭代序列的大小:${result.value}`); // 5

备注: 反射性地知道特定对象是否是迭代器是不可能的。如果你需要这样做,请使用可迭代对象

生成器函数 function*

虽然自定义迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此创建时要格外谨慎。生成器函数(Generator 函数)提供了一个强大的替代选择:它允许你定义一个非连续执行的函数作为迭代算法。生成器函数使用 function* 语法编写。

最初调用时,生成器函数不执行任何代码,而是返回一种称为生成器的特殊迭代器。通过调用 next() 方法消耗该生成器时,生成器函数将执行,直至遇到 yield 关键字。

可以根据需要多次调用该函数,并且每次都返回一个新的生成器,但每个生成器只能迭代一次。

我们现在可以调整上面的例子了。此代码的行为并没有改变,但更容易编写和阅读。

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 中会循环一些值,那么那个对象便是一个可迭代对象。一些内置类型,如 Array 或 Map 拥有默认的迭代行为,而其他类型(比如 Object)则没有。

为了实现可迭代,一个对象必须实现 @@iterator 方法,这意味着这个对象(或其原型链中的任意一个对象)必须具有一个键值为 Symbol.iterator 的属性。

程序员应知道一个可迭代对象可以多次迭代,还是只能迭代一次。

只能迭代一次的可迭代对象(例如 Generator)通常从它们的 @@iterator 方法中返回 this,而那些可以多次迭代的方法必须在每次调用 @@iterator 时返回一个新的迭代器。

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

const it = makeIterator();

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

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

// 这个例子向我们展示了生成器(迭代器)是可迭代对象,
// 它有一个 @@iterator 方法返回 it(它自己),
// 因此,it 对象只能迭代*一次*。

// 如果我们将它的 @@iterator 方法改为一个返回新的迭代器/生成器对象的函数/生成器,
// 它(it)就可以迭代多次了。

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

自定义的可迭代对象

我们可以像这样实现自己的可迭代对象:

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

自定义的可迭代对象可用 for...of 循环或者展开语法进行迭代。

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

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

内置可迭代对象

StringArrayTypedArrayMap 和 Set 都是内置可迭代对象,因为它们的原型对象都拥有一个 Symbol.iterator 方法。

用于可迭代对象的语法

一些语句和表达式专用于可迭代对象,例如 for...of 循环、展开语法yield* 和解构语法。

js
for (let 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() 方法可返回给定的值并终结这个生成器。

评论区

欢迎留言、补充或勘误。

xiaoba.blog