手把手带你理解JS中迭代器与生成器

469 阅读5分钟

JavaScript 是一门充满“黑科技”的语言,而迭代器(Iterator)和生成器(Generator)就是其中的两个魔法工具。它们不仅让代码更优雅,还能显著提高效率。今天我们就深入挖掘一下这两个工具,看它们如何在实际开发中为我们所用。

迭代器(Iterator):逐步处理数据的艺术

什么是迭代器?

迭代器的概念其实并不复杂。想象一下你在看一本书,你不可能一下子读完整本书,而是通过一页一页地读完。迭代器就是这样的工具,它可以让你逐步处理一个数据集合,而不是一次性加载所有数据。每次调用迭代器的 next()方法,它都会返回集合中的下一个值,直到处理完所有数据。

在 JavaScript 中,迭代器是一种特殊的对象,它具有一个next()方法,每次调用这个方法,都会返回一个包含valuedone属性的对象:

  • value 是当前的值。
  • done 是一个布尔值,表示迭代是否完成。

举个例子: 我们可以手动创建一个迭代器来理解它的工作方式:

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

const iterator = createIterator(["🚀", "🌕", "🛸"]);

console.log(iterator.next()); // { value: '🚀', done: false }
console.log(iterator.next()); // { value: '🌕', done: false }
console.log(iterator.next()); // { value: '🛸', done: false }
console.log(iterator.next()); // { value: undefined, done: true }

每次调用next(),你都可以获取下一个元素,直到没有元素可迭代为止。

为什么要用迭代器?

在处理大量数据或需要按需生成数据时,迭代器特别有用。它避免了一次性加载大量数据到内存中,尤其是在处理像分页、流媒体数据或无限数据流时非常实用。

假设我们有一个大型数据集合,但我们不希望一次性加载所有数据,而是希望分页加载:

function createPaginator(data, pageSize) {
  let currentPage = 0;
  return {
    next: function () {
      const start = currentPage * pageSize;
      const end = start + pageSize;
      currentPage++;
      return start < data.length
        ? { value: data.slice(start, end), done: false }
        : { value: [], done: true };
    },
  };
}

const data = Array.from({ length: 100 }, (_, i) => `Item ${i + 1}`);
const paginator = createPaginator(data, 10);

console.log(paginator.next().value); // ['Item 1', 'Item 2', ..., 'Item 10']
console.log(paginator.next().value); // ['Item 11', 'Item 12', ..., 'Item 20']
// ...直到没有更多数据为止

通过这种方式,我们可以灵活地分页加载数据,而不是一次性加载全部,从而减少内存占用,提升页面响应速度。

生成器(Generator):控制流程的超级工具

什么是生成器?

生成器是迭代器的“进阶版”。生成器函数用function*定义,它可以暂停执行,并且可以通过yield关键字一次返回一个值。与普通函数不同,生成器函数在执行时不会一次性执行完毕,而是在每次调用next()时,执行到yield表达式暂停,返回yield的值,直到执行完毕。

基本示例 让我们看看生成器是如何工作的:

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

const gen = numberGenerator();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

生成器每次调用next()都会从上次暂停的地方继续执行,直到没有更多值为止。

哪些场景需要生成器

无限序列生成

生成器可以轻松创建无限序列,比如斐波那契数列或其他数学序列:

function* fibonacciGenerator() {
  let [prev, curr] = [0, 1];
  while (true) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

const fib = fibonacciGenerator();

console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
console.log(fib.next().value); // 5
// 无限输出下去...

这个生成器可以无限地生成斐波那契数列,而不会耗尽内存。

处理异步任务

生成器的强大之处还在于它能够暂停和恢复,这使得它非常适合处理异步任务。我们可以将生成器与Promise结合,创建一个异步任务的执行器:

function* asyncTaskRunner() {
  const result1 = yield new Promise((resolve) =>
    setTimeout(() => resolve("Task 1 completed"), 1000)
  );
  console.log(result1);
  const result2 = yield new Promise((resolve) =>
    setTimeout(() => resolve("Task 2 completed"), 1000)
  );
  console.log(result2);
}

function runGenerator(gen) {
  const iterator = gen();

  function handleNext(result) {
    if (result.done) return;
    result.value.then((res) => handleNext(iterator.next(res)));
  }

  handleNext(iterator.next());
}

runGenerator(asyncTaskRunner);
// 1秒后输出:Task 1 completed
// 再过1秒输出:Task 2 completed

通过这种方式,我们可以写出类似同步代码的异步逻辑,避免“回调地狱”,让代码更易读、更易维护。

状态机

使用生成器实现一个具有多个状态和事件处理的复杂状态机,可以通过定义一个生成器函数来管理状态转换和事件处理。以下是一个更详细的示例,展示了如何使用生成器来实现一个具有多个状态和事件的复杂状态机:

// 定义事件类型
const EVENTS = {
  START_PROCESS: "start_process",
  STOP_PROCESS: "stop_process",
  ERROR: "error",
  FINISH: "finish",
};

// 定义状态
const STATES = {
  IDLE: "idle",
  RUNNING: "running",
  ERRORED: "errored",
  DONE: "done",
};

// 状态机生成器函数
function* stateMachine() {
  let state = STATES.IDLE;

  while (true) {
    console.log(`状态: ${state}`);
    // 根据不同的状态,处理不同的事件
    switch (state) {
      case STATES.IDLE:
        const event = yield { state, message: "等待开始" };
        if (event.type === EVENTS.START_PROCESS) {
          state = STATES.RUNNING;
        }
        break;
      case STATES.RUNNING:
        const event = yield { state, message: "正在处理" };
        if (event.type === EVENTS.STOP_PROCESS) {
          state = STATES.IDLE;
        } else if (event.type === EVENTS.ERROR) {
          state = STATES.ERRORED;
        } else if (event.type === EVENTS.FINISH) {
          state = STATES.DONE;
        }
        break;
      case STATES.ERRORED:
        console.log("发生错误,状态机停止");
        return;
      case STATES.DONE:
        console.log("处理完成,状态机停止");
        return;
      default:
        console.log("未知状态");
        return;
    }
  }
}

// 使用生成器
const machine = stateMachine();

// 启动状态机
console.log(machine.next().value); // 等待开始

// 模拟事件处理
machine.next({ type: EVENTS.START_PROCESS }).value; // 正在处理
machine.next({ type: EVENTS.ERROR }).value; // 发生错误,状态机停止

// 如果没有错误,可以继续模拟其他事件
// machine.next({ type: EVENTS.STOP_PROCESS }).value; // 等待开始
// machine.next({ type: EVENTS.START_PROCESS }).value; // 正在处理
// machine.next({ type: EVENTS.FINISH }).value; // 处理完成,状态机停止

在这个示例中,我们首先定义了事件类型和状态。然后,我们创建了一个生成器函数 stateMachine,它使用一个 while(true)循环来模拟状态机的持续运行。在每个状态中,我们使用 yield 来暂停执行并等待下一个事件。

事件以对象的形式传递给 yield,包含事件类型和其他相关信息。状态机根据当前状态和接收到的事件来决定下一个状态。

使用生成器的好处是,你可以很容易地暂停和恢复状态机的执行,并且可以在状态机的任何地方保存和恢复状态,这使得它非常适合处理复杂的状态逻辑和异步事件。

请注意,这个示例是一个简化的版本,实际应用中的状态机可能需要处理更多的状态和事件,并且可能需要与外部系统或用户交互。

迭代器与生成器的联动

迭代器和生成器可以一起使用,达到更强大的效果。生成器不仅可以创建迭代器,还能通过外部输入控制迭代器的行为。

实际场景

复杂数据流处理

假设我们有一个数据流,需要根据不同条件进行处理并输出结果,我们可以使用生成器和迭代器结合来实现:

function* dataProcessor(data) {
  for (let item of data) {
    if (item % 2 === 0) {
      yield `Even number: ${item}`;
    } else {
      yield `Odd number: ${item}`;
    }
  }
}

const data = [1, 2, 3, 4, 5, 6];
const processor = dataProcessor(data);

for (let output of processor) {
  console.log(output);
}

// 输出:
// Odd number: 1
// Even number: 2
// Odd number: 3
// Even number: 4
// Odd number: 5
// Even number: 6

通过生成器的灵活性,我们可以轻松处理复杂的数据流逻辑,让代码更加简洁。

总结

迭代器和生成器是 JavaScript 中非常实用的工具,特别是在处理复杂的数据结构、异步任务、无限序列等场景时,能够极大地简化代码,提高效率。

  • 迭代器:适用于分页加载、逐步处理数据等场景,能够避免一次性加载大量数据,提高内存利用率。
  • 生成器:更灵活强大,适用于生成无限序列、处理复杂异步任务、构建状态机等场景,代码易读易维护。

当你下次在项目中遇到需要处理大量数据、复杂异步逻辑或者需要实现自定义迭代逻辑的情况时,别忘了迭代器和生成器这两把利器,它们会让你的代码变得更加优雅和高效。像吃披萨一样,一口一口慢慢享用,迭代器和生成器会让你每一口都充满惊喜!