JavaScript 迭代器与生成器:控制异步与惰性求值

1. 引言:为什么需要迭代器与生成器

在 JavaScript 中,我们每天都在和"可遍历的东西"打交道:数组、字符串、Map、Set、arguments,甚至 NodeList。for...of 能统一地遍历它们,背后靠的正是可迭代协议 与迭代器协议。

而当我们面对"数据量很大、无法一次性加载"或"异步数据源源不断"的场景时,普通的数组遍历就显得力不从心了。这时,生成器(Generator) 与 异步生成器(Async Generator) 提供了优雅的解决方案:它们让函数可以"暂停---恢复",从而实现惰性求值 与异步流程控制。

本文将从协议出发,逐步深入到生成器、yield、异步生成器,并给出可直接运行的示例。

2. 可迭代协议与迭代器协议

JavaScript 的遍历能力建立在两个协议之上,理解它们是掌握后续内容的基础。

两个协议的关系可以这样理解:

flowchart LR A[可迭代对象] --> B[返回迭代器] B --> C[value done 结果] C --> D[继续取值] C --> E[迭代结束] D --> B

2.1 可迭代协议(Iterable Protocol)

一个对象只要实现了 Symbol.iterator 方法,并且该方法返回一个迭代器,它就是可迭代对象。

javascript 复制代码
const arr = [1, 2, 3];
console.log(typeof arr[Symbol.iterator]); // "function"

const iter = arr[Symbol.iterator]();
console.log(iter.next()); // { value: 1, done: false }
console.log(iter.next()); // { value: 2, done: false }
console.log(iter.next()); // { value: 3, done: false }
console.log(iter.next()); // { value: undefined, done: true }

for...of、展开运算符 ...、解构赋值、Array.from 等语法,本质上都是在调用对象的 Symbol.iterator。

2.2 迭代器协议(Iterator Protocol)

迭代器是一个对象,它必须实现 next() 方法,返回形如 { value, done } 的结果:

  • value:当前产出的值;
  • done:布尔值,true 表示迭代结束。
javascript 复制代码
function createRangeIterator(start, end) {
  let current = start;
  return {
    next() {
      if (current <= end) {
        return { value: current++, done: false };
      }
      return { value: undefined, done: true };
    },
    // 让迭代器自身也可迭代,便于 for...of 使用
    [Symbol.iterator]() {
      return this;
    },
  };
}

for (const n of createRangeIterator(1, 5)) {
  console.log(n); // 1 2 3 4 5
}

2.3 自定义可迭代对象

把两个协议结合起来,就能让任意对象支持 for...of:

javascript 复制代码
const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      },
    };
  },
};

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

3. 生成器函数:更简洁的迭代器工厂

手写迭代器虽然直观,但状态管理繁琐。生成器函数 用 function* 声明,内部通过 yield 产出值,调用时返回一个迭代器,代码写起来像同步函数一样自然。

javascript 复制代码
function* rangeGenerator(from, to) {
  for (let i = from; i <= to; i++) {
    yield i;
  }
}

const gen = rangeGenerator(1, 5);
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log([...rangeGenerator(1, 5)]); // [1, 2, 3, 4, 5]

生成器函数有几个关键特性:

  • 调用生成器函数不会立即执行函数体,而是返回一个迭代器;
  • 每次调用 next() 才执行到下一个 yield 处并暂停;
  • 函数体执行完毕后,done 变为 true。

3.1 生成器的执行流程

flowchart TD A[调用 gen] --> B[返回迭代器对象] B --> C[gen next] C --> D[执行到 yield 并暂停] D --> E{还有 yield 吗} E --> C E --> F[返回 done true]

4. 深入 yield:双向通信与委托

yield 不只是"产出值",它还是一个双向通道 :next(value) 传入的参数会成为上一个 yield 表达式的返回值。

4.1 yield 作为表达式

javascript 复制代码
function* dialog() {
  const name = yield "你叫什么名字?";
  const age = yield `你好,${name}!你多大了?`;
  return `${name},${age} 岁,很高兴认识你。`;
}

const it = dialog();
console.log(it.next());        // { value: "你叫什么名字?", done: false }
console.log(it.next("小明"));  // { value: "你好,小明!你多大了?", done: false }
console.log(it.next(18));      // { value: "小明,18 岁,很高兴认识你。", done: true }

注意:第一次 next() 的参数会被忽略,因为它之前没有 yield 在等待接收。

4.2 yield* 委托

yield* 可以把迭代控制权委托给另一个可迭代对象:

javascript 复制代码
function* inner() {
  yield 1;
  yield 2;
}

function* outer() {
  yield 0;
  yield* inner();
  yield 3;
}

console.log([...outer()]); // [0, 1, 2, 3]

4.3 return 与 throw

生成器迭代器还支持 return() 和 throw(),可用于提前终止或向生成器内部抛错:

javascript 复制代码
function* task() {
  try {
    yield 1;
    yield 2;
  } finally {
    console.log("清理资源");
  }
}

const t = task();
console.log(t.next()); // { value: 1, done: false }
console.log(t.return(99)); // 触发 finally,输出"清理资源",返回 { value: 99, done: true }

5. 惰性求值:生成器的核心价值

生成器最大的优势是按需计算 。它不会一次性把所有结果算出来,而是每次 next() 才算一个,这对无限序列和大数据处理非常友好。

5.1 无限序列

javascript 复制代码
function* naturals() {
  let n = 0;
  while (true) {
    yield n++;
  }
}

const nums = naturals();
console.log(nums.next().value); // 0
console.log(nums.next().value); // 1
console.log(nums.next().value); // 2

5.2 惰性管道

把多个生成器串联,可以构建惰性数据管道,只有真正取值时才计算:

javascript 复制代码
function* map(iterable, fn) {
  for (const item of iterable) {
    yield fn(item);
  }
}

function* filter(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) yield item;
  }
}

function* take(iterable, n) {
  let count = 0;
  for (const item of iterable) {
    if (count++ >= n) return;
    yield item;
  }
}

const pipeline = take(
  filter(map(naturals(), (x) => x * x), (x) => x % 2 === 0),
  5
);

console.log([...pipeline]); // [0, 4, 16, 36, 64]

上面的例子从无限自然数中取平方、筛偶数、只取前 5 个,全程没有产生多余计算。

惰性管道的取值过程如下,数据只有在被消费时才逐级流动:

flowchart LR A[naturals 无限自然数] --> B[map 平方] B --> C[filter 筛偶数] C --> D[take 前五个] D --> E[消费端展开 pipeline] E --> A

6. 异步生成器:处理流式异步数据

当数据是异步到达的(如分页接口、WebSocket 消息、文件流),普通生成器就不够用了。异步生成器 用 async function* 声明,配合 for await...of 使用。

同步迭代与异步迭代的差异可以用一张图对比:

flowchart TD A[消费端 for await of] --> B[调用 asyncIterator next] B --> C[返回 Promise] C --> D[await 等待异步数据就绪] D --> E{done 为 true 吗} E --> F[产出 value 并处理] F --> B E --> G[结束迭代]

6.1 基本用法

javascript 复制代码
async function* fetchPages() {
  for (let page = 1; page <= 3; page++) {
    const data = await fakeFetch(page);
    yield data;
  }
}

function fakeFetch(page) {
  return new Promise((resolve) =>
    setTimeout(() => resolve(`第 ${page} 页数据`), 300)
  );
}

async function main() {
  for await (const page of fetchPages()) {
    console.log(page);
  }
}

main();
// 第 1 页数据
// 第 2 页数据
// 第 3 页数据

6.2 异步迭代器协议

异步生成器返回的是异步迭代器 ,它的 next() 返回 Promise<{ value, done }>,并且通过 Symbol.asyncIterator 标识:

javascript 复制代码
async function* gen() {
  yield 1;
  yield 2;
}

const asyncIter = gen();
console.log(typeof asyncIter[Symbol.asyncIterator]); // "function"

asyncIter.next().then((res) => console.log(res)); // { value: 1, done: false }

6.3 异步惰性管道

异步生成器同样可以组合成惰性管道,非常适合处理流式数据:

javascript 复制代码
async function* asyncMap(iterable, fn) {
  for await (const item of iterable) {
    yield fn(item);
  }
}

async function* asyncFilter(iterable, predicate) {
  for await (const item of iterable) {
    if (predicate(item)) yield item;
  }
}

async function* source() {
  for (let i = 1; i <= 10; i++) {
    await new Promise((r) => setTimeout(r, 100));
    yield i;
  }
}

async function run() {
  const result = asyncFilter(
    asyncMap(source(), (x) => x * 10),
    (x) => x % 20 === 0
  );
  for await (const v of result) {
    console.log(v); // 20 40 60 80 100
  }
}

run();

7. 实战:用异步生成器实现分页拉取

下面是一个更贴近真实业务的例子:分页拉取数据,直到没有更多为止,并在消费端做惰性处理。

javascript 复制代码
async function* paginate(fetchPage, pageSize = 10) {
  let page = 1;
  while (true) {
    const list = await fetchPage(page, pageSize);
    if (!list || list.length === 0) return;
    for (const item of list) {
      yield item;
    }
    if (list.length < pageSize) return;
    page++;
  }
}

// 模拟接口
async function fetchPage(page, size) {
  await new Promise((r) => setTimeout(r, 200));
  if (page > 3) return [];
  return Array.from({ length: size }, (_, i) => `item-${(page - 1) * size + i + 1}`);
}

async function consume() {
  let count = 0;
  for await (const item of paginate(fetchPage, 5)) {
    console.log(item);
    if (++count >= 8) break; // 消费端可随时中断,避免多余请求
  }
}

consume();

这个模式的好处是:消费端按需拉取 ,一旦 break,生成器内部的循环也会随之终止,不会继续发起无意义的请求。

分页拉取与消费端中断的协作过程如下:

sequenceDiagram participant C as 消费端 consume participant G as paginate 生成器 participant API as fetchPage 接口 C->>G: for await 请求下一个 item G->>API: 拉取第一页 API-->>G: 返回列表 G-->>C: yield 单个 item C->>G: 继续请求 G->>API: 拉取下一页 API-->>G: 返回列表 G-->>C: yield item C->>G: count 达到 8 触发 break G->>G: 终止循环不再请求

8. 常见陷阱与最佳实践

  • 生成器是一次性的:迭代器耗尽后无法重置,需要重新调用生成器函数。
  • 不要忘记 return 的清理逻辑 :在 try/finally 中释放资源,return() 或 break 都会触发 finally。
  • 避免在生成器中做副作用过重的操作:惰性求值意味着执行时机不确定,副作用要谨慎。
  • 异步生成器中的错误处理 :用 try/catch 包裹 for await...of,或让生成器内部 throw。
  • 性能考量:生成器有额外的调用开销,对极小数据量不必强行使用。
javascript 复制代码
async function safeConsume() {
  try {
    for await (const item of paginate(fetchPage, 5)) {
      console.log(item);
    }
  } catch (err) {
    console.error("拉取失败:", err);
  }
}

9. 总结

概念 关键点 典型场景
可迭代协议 实现 Symbol.iterator 自定义对象支持 for...of
迭代器协议 实现 next() 返回 { value, done } 手动控制遍历
生成器函数 function* + yield 简化迭代器编写
yield 双向通信、yield* 委托 状态机、协程式流程
异步生成器 async function* + for await...of 分页、流式数据

迭代器与生成器把"遍历"从一次性计算变成了可控的、可暂停的、可组合的过程。掌握它们,不仅能写出更优雅的惰性求值代码,也能在异步数据流处理中游刃有余。

相关推荐
Hilaku24 分钟前
Event Loop 这篇你看完就不用再看别的了!
前端·javascript·程序员
恋猫de小郭1 小时前
Flutter 下 AOT 一个你怎么都想不出来的 Bug ,JIT 正常的计算,在 AOT 居然算错了
android·前端·flutter
晓得迷路了1 小时前
栗子前端技术周刊第 153 期 - Vitest 5.1、Remix v3.2、Node.js 27 Alpha...
前端·javascript·vite
系统集成-鹰翔2 小时前
ERP单据已作废为什么商品仍不能删除,引用检查与软删除怎么查
javascript·经验分享·后端
Moment2 小时前
Harness Engineering 为什么是 AI Coding 的工程底座 ❓❓❓
前端·后端·面试
Penge6662 小时前
《Go 后端开发者学 React:Hook》
javascript
Penge6662 小时前
《Go 后端开发者学 React:前端 HTTP 请求详解》
javascript
风骏时光牛马2 小时前
生成一个AI面试复盘标题,只需要一个标题,不需要其他的任何文本。
前端