ES6 迭代器与生成器

迭代器与生成器

    • [ES6 迭代器与生成器深度解析](#ES6 迭代器与生成器深度解析)
    • 一、迭代器(Iterator)
      • [1.1 核心概念](#1.1 核心概念)
      • [1.2 最基本的迭代器使用](#1.2 最基本的迭代器使用)
      • [1.3 `for...of` 循环](#1.3 for...of 循环)
      • [1.4 展开运算符 `...` 与可迭代对象](#1.4 展开运算符 ... 与可迭代对象)
      • [1.5 自定义迭代器](#1.5 自定义迭代器)
    • 二、生成器(Generator)
      • [2.1 基本语法与行为](#2.1 基本语法与行为)
      • [2.2 生成器是迭代器的工厂](#2.2 生成器是迭代器的工厂)
      • [2.3 `yield*` 委托](#2.3 yield* 委托)
      • [2.4 向生成器传入值:`next(value)`](#2.4 向生成器传入值:next(value))
      • [2.5 生成器的 `return()` 和 `throw()`](#2.5 生成器的 return()throw())
    • 三、深入应用场景
      • [3.1 惰性求值与无限序列](#3.1 惰性求值与无限序列)
      • [3.2 实现可迭代的数据结构](#3.2 实现可迭代的数据结构)
      • [3.3 异步流程控制(生成器 + Promise)](#3.3 异步流程控制(生成器 + Promise))
    • 四、总结对比

ES6 迭代器与生成器深度解析

在 ES6 之前,遍历一个数据集合(比如数组、对象、Map、Set)往往需要不同的方式:forfor...in、``Array.prototype.forEach` 等。ES6 引入了一套统一的迭代协议 ,让任何数据结构都可以被标准地遍历。而生成器则是创建迭代器最便捷、最强大的工具。


一、迭代器(Iterator)

1.1 核心概念

  • 迭代器协议:定义了一种标准方式来产生一个有限或无限序列的值。当一个对象满足以下条件时,它就是一个迭代器:

    • 它有一个 next() 方法,该方法返回一个对象 { value: any, done: boolean }
    • donetrue 表示迭代结束,value 可以省略(通常为 undefined)。
    • donefalse 或未提供时,value 是本次迭代的值。
  • 可迭代协议 :允许 JavaScript 对象定义或定制它们的迭代行为。一个对象如果具有 Symbol.iterator 属性,且该属性是一个返回迭代器的无参函数,那么这个对象就是可迭代的(iterable)。

许多内置类型默认就是可迭代的:ArrayStringMapSetarguments 对象、NodeList 等。

1.2 最基本的迭代器使用

javascript 复制代码
// 数组本身是可迭代的,我们可以手动获取它的迭代器
const arr = ['a', 'b', 'c'];
const iterator = arr[Symbol.iterator]();

console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }

1.3 for...of 循环

for...of 是专门用来遍历可迭代对象的语法糖,它自动调用 Symbol.iterator 并反复调用 next(),直到 donetrue

javascript 复制代码
const arr = ['a', 'b', 'c'];
for (const item of arr) {
  console.log(item);
}
// 输出 a b c

// 字符串也是可迭代的
for (const ch of 'Hello') {
  console.log(ch);
}
// H e l l o

1.4 展开运算符 ... 与可迭代对象

展开运算符内部也使用了迭代器协议,因此可以展开任何可迭代对象。

javascript 复制代码
const set = new Set([1, 2, 3]);
const arr = [...set]; // [1, 2, 3]

// 甚至可以把字符串展开成字符数组
const chars = [...'abc']; // ['a', 'b', 'c']

1.5 自定义迭代器

假设我们想要一个对象,它可以产生从 startend 的整数序列。我们可以手动实现 Symbol.iterator 方法。

javascript 复制代码
const range = {
  start: 1,
  end: 5,
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        } else {
          return { done: true };
        }
      },
    };
  },
};

for (const num of range) {
  console.log(num);
}
// 输出 1 2 3 4 5

// 也可以手动迭代
const it = range[Symbol.iterator]();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
// ...

注意:迭代器本身通常也可以是一个可迭代对象(即它也有 Symbol.iterator 方法,返回 this),这样迭代器可以直接用于 for...of。上面的例子中,我们返回的迭代器对象没有实现 Symbol.iterator,所以它本身不是可迭代的,但通常我们只关心 range 是可迭代的。


二、生成器(Generator)

手动构建一个遵循迭代器协议的对象往往需要编写很多模板代码。生成器函数 提供了一种更简洁、更强大的方式------使用 function* 定义,内部通过 yield 关键字控制执行流。

2.1 基本语法与行为

  • 生成器函数:function* name() { ... }
  • 调用生成器函数不会立即执行 函数体,而是返回一个生成器对象(Generator Object)。
  • 生成器对象同时实现了迭代器协议可迭代协议
  • 每次调用生成器对象的 next() 方法,函数体会从上一次暂停的位置(或开头)执行,直到遇到下一个 yieldreturn
  • yield 表达式可以返回一个值,并暂停函数执行。
  • return 语句会终结生成器,done 变为 truevaluereturn 的值(若省略则为 undefined)。
javascript 复制代码
function* simpleGenerator() {
  console.log('开始执行');
  yield 1;
  console.log('第一次恢复');
  yield 2;
  console.log('第二次恢复');
  return 3; // 结束,done 变为 true,value 为 3
}

const gen = simpleGenerator();
console.log(gen.next()); 
// 输出:开始执行
// { value: 1, done: false }

console.log(gen.next());
// 输出:第一次恢复
// { value: 2, done: false }

console.log(gen.next());
// 输出:第二次恢复
// { value: 3, done: true }

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

2.2 生成器是迭代器的工厂

生成器函数最大的优势就是可以轻松创建自定义迭代器。我们用生成器重写之前的 range

javascript 复制代码
function* range(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

const it = range(1, 5);
for (const num of it) {
  console.log(num);
}
// 1 2 3 4 5

// 或者直接用 for...of 迭代生成器函数返回的对象
for (const num of range(3, 7)) {
  console.log(num);
}
// 3 4 5 6 7

可以看到,代码极其简洁,不再需要手动管理 next 和状态。

2.3 yield* 委托

在生成器内部,可以使用 yield* 表达式将迭代控制权委托给另一个可迭代对象(或生成器)。这相当于把那个可迭代对象的所有值逐个 yield 出来。

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

function* generateMore() {
  yield 'a';
  yield* generateNumbers(); // 委托
  yield 'b';
  yield* [10, 20]; // 委托给数组
}

for (const val of generateMore()) {
  console.log(val);
}
// 输出:a, 1, 2, b, 10, 20

2.4 向生成器传入值:next(value)

next() 方法可以接收一个参数,这个参数会作为上一个 yield 表达式的返回值 (在生成器内部)。第一次调用 next() 时传参会被忽略,因为此时还没有上一个 yield

这个特性使得生成器可以接收外部数据,实现双向通信。

javascript 复制代码
function* twoWay() {
  const first = yield '请输入第一个数';
  const second = yield '请输入第二个数';
  yield `和是 ${first + second}`;
}

const gen = twoWay();
console.log(gen.next());       // { value: '请输入第一个数', done: false }
console.log(gen.next(10));     // 10 被赋给 first,然后执行到下一个 yield
// { value: '请输入第二个数', done: false }
console.log(gen.next(20));     // 20 被赋给 second,执行 yield 和
// { value: '和是 30', done: false }
console.log(gen.next());       // { value: undefined, done: true }

注意:next() 传入的值会替换生成器内部对应的 yield 表达式。第一次 next() 调用时没有等待中的 yield,所以参数被忽略。

2.5 生成器的 return()throw()

  • generator.return(value):强制生成器结束,并返回 { value, done: true }。后续 next() 调用都会返回 { done: true }
  • generator.throw(error):在生成器当前暂停的位置抛出一个错误,如果生成器内部捕获了该错误,则可以继续执行;否则生成器终止。
javascript 复制代码
function* withError() {
  try {
    yield 1;
    yield 2;
  } catch (e) {
    console.log('捕获到错误:', e.message);
    yield 3;
  }
  yield 4;
}

const gen = withError();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.throw(new Error('出错了'))); 
// 捕获到错误:出错了
// { value: 3, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: undefined, done: true }

三、深入应用场景

3.1 惰性求值与无限序列

生成器可以表示无穷序列,因为它只在需要时计算下一个值。这避免了预先分配大量内存。

示例:无限斐波那契数列

javascript 复制代码
function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
// 可以一直取下去,但只占用 O(1) 内存

// 取前 10 个斐波那契数
const first10 = [];
for (let i = 0; i < 10; i++) {
  first10.push(fib.next().value);
}
console.log(first10); // [5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
// 注意:上面已经消耗了前 5 个,所以从第 6 个开始

3.2 实现可迭代的数据结构

假设我们有一个自定义的链表或树,我们可以用生成器轻松定义深度优先遍历。

javascript 复制代码
class TreeNode {
  constructor(value, children = []) {
    this.value = value;
    this.children = children;
  }

  *[Symbol.iterator]() {
    yield this.value;
    for (const child of this.children) {
      yield* child; // 委托给子节点的迭代器
    }
  }
}

// 构建树:根节点 1,子节点 2、3,其中 2 有子节点 4、5
const tree = new TreeNode(1, [
  new TreeNode(2, [new TreeNode(4), new TreeNode(5)]),
  new TreeNode(3),
]);

for (const val of tree) {
  console.log(val);
}
// 输出:1 2 4 5 3 (前序遍历)

3.3 异步流程控制(生成器 + Promise)

async/await 出现之前,生成器配合 Promise 可以实现类似"同步写法"的异步代码。著名的 co 库就是基于这个思想。

原理 :生成器内部 yield 一个 Promise,外部控制函数负责 next 并把 Promise 结果传回去。

javascript 复制代码
// 模拟异步任务
function fetchData(url) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`数据来自 ${url}`), 1000);
  });
}

// 使用生成器编写异步流程
function* asyncFlow() {
  const data1 = yield fetchData('https://api.example.com/user');
  console.log(data1);
  const data2 = yield fetchData('https://api.example.com/posts');
  console.log(data2);
  return '完成';
}

// 自动执行器(简化版)
function run(generatorFunc) {
  const gen = generatorFunc();
  function handle(result) {
    if (result.done) return result.value;
    // 假设 yield 出来的总是 Promise
    result.value.then(
      (res) => handle(gen.next(res)),
      (err) => gen.throw(err)
    );
  }
  handle(gen.next());
}

run(asyncFlow);
// 1秒后输出:数据来自 https://api.example.com/user
// 再1秒后输出:数据来自 https://api.example.com/posts

这正是 async/await 的前身,只不过现在语法更优雅。


四、总结对比

特性 手动实现迭代器 生成器
代码量 较多,需维护状态和 next 方法 极少,只需 function* + yield
可读性 一般 非常直观,像同步函数
是否支持双向通信 否(除非额外实现) 是,通过 next(value)
是否支持惰性求值 可以,但需手动控制 天然支持,yield 即暂停
能否表示无限序列 可以,但需谨慎处理结束条件 可以,用 while(true) 非常自然
是否可被 for...of 需要同时实现可迭代协议 生成器对象本身就是可迭代的

核心要点记忆

  1. 可迭代对象Symbol.iterator 方法,返回一个迭代器
  2. 迭代器next() 方法,返回 { value, done }
  3. 生成器函数function*)调用后返回一个生成器对象,它既是迭代器也是可迭代对象。
  4. yield 让生成器可以暂停和恢复,yield* 委托给其他可迭代对象。
  5. 生成器可以用于惰性序列自定义遍历 以及简化异步控制流 (尽管现在更推荐直接用 async/await)。
相关推荐
周周记笔记2 小时前
初识HTML和CSS(一)
前端·css·html
aq55356002 小时前
网页开发四剑客:HTML/CSS/JS/PHP全解析
javascript·css·html
程序员buddha2 小时前
TypeScript详细教程
javascript·ubuntu·typescript
chxii3 小时前
在 IIS 中实现 SSL 证书的自动续期
前端
周星星日记3 小时前
vue3中静态提升和patchflag实现
前端·vue.js·面试
橘子编程3 小时前
React 19 全栈开发实战指南
前端·react.js·前端框架
DanCheOo3 小时前
AI Streaming 架构:从浏览器到服务端的全链路流式设计
前端·agent
我是小趴菜3 小时前
前端如何让图片、视频、pdf等文件在浏览器直接下载而非预览
前端
cg333 小时前
开源项目自动化:用 GitHub Actions 让每个 Issue 都被温柔以待
前端