Skip to content

5.2 迭代器模式 (Iterator Pattern)

欢迎来到迭代器模式的学习!这是一个旨在统一数据结构遍历方式的模式。在现代 JavaScript 中,它已经不再仅仅是一个“模式”,而是融入语言核心的“协议” (Protocol)。

核心定义:提供一种方法来顺序访问一个聚合对象(集合)中的各个元素,而又不需要暴露该对象的内部表示(例如,它是数组、链表还是树)。

一、生活中的场景:万能电视遥控器

想象一下你家客厅里有三台不同品牌的电视:一台索尼(Sony),一台三星(Samsung),一台小米(Xiaomi)。

  • 内部构造不同:它们的内部电路、操作系统(数据结构)完全不一样。
  • 遍历需求相同:但你对它们都有一个共同的需求——“切换到下一个频道”。

如果每台电视都需要一个专属的遥控器才能换台,那将非常麻烦。一个万能遥控器Iterator / 迭代器)解决了这个问题。它提供了一个统一的“下一个”(next())按钮,无论你对着哪台电视,它都能帮你切换到下一个频道。

这个遥控器,就是迭代器。它封装了不同电视内部切换频道的复杂逻辑,为使用者提供了统一的、简单的遍历接口

二、没有迭代器模式的代码:混乱的遍历逻辑

假设我们有两个不同的数据集合:一个存储员工列表的数组(Array),和一个存储部门信息的 Map。我们想写一个函数来打印出所有的成员。

javascript
const staff = ["张三", "李四", "王五"];
const departments = new Map();
departments.set("rd", { name: "研发部", count: 50 });
departments.set("hr", { name: "人事部", count: 5 });

// 遍历并打印所有成员
function printAllMembers(staffList, departmentMap) {
  // --- 遍历数组 ---
  console.log("--- 员工列表 ---");
  for (let i = 0; i < staffList.length; i++) {
    console.log(staffList[i]);
  }

  // --- 遍历 Map ---
  console.log("--- 部门信息 ---");
  // 遍历 Map 的方式和数组完全不同!
  departmentMap.forEach((dept, key) => {
    console.log(`${key}: ${dept.name}`);
  });
}

printAllMembers(staff, departments);

这段代码的问题在于,客户端(printAllMembers 函数)被迫要知道它所处理的数据结构的具体类型和内部实现。它必须为 Array 写一套遍历逻辑,为 Map 写另一套。如果再来一个 Set 或者我们自定义的数据结构,这个函数就得继续膨胀。

三、ES6 迭代器协议:语言层面的统一

幸运的是,现代 JavaScript (ES6+) 在语言层面就为我们内置了迭代器模式,它被称为“可迭代协议” (Iterable Protocol)

这个协议规定:

  1. 一个对象如果想成为可迭代的 (Iterable),它必须实现一个键为 [Symbol.iterator] 的方法。
  2. 这个 [Symbol.iterator] 方法必须返回一个迭代器对象 (Iterator)
  3. 这个迭代器对象必须有一个 next() 方法。
  4. next() 方法返回一个格式为 { value: '当前值', done: boolean } 的对象。donetrue 时表示遍历结束。

Array, String, Map, Set 等原生数据结构,都已经默认实现了这个协议。这正是为什么它们都可以被 for...of 循环统一遍历的原因!

使用 for...of 统一遍历

for...of 循环是可迭代协议的语法糖。它会自动调用对象的 [Symbol.iterator] 方法,获取迭代器,然后不断调用 next() 并取出 value,直到 donetrue

javascript
// 客户端代码现在变得无比简洁和统一
function printAllMembers(...collections) {
  for (const collection of collections) {
    console.log("--- 开始遍历 ---");
    // for...of 不关心 collection 是数组、Map 还是 Set
    // 它只关心 collection 是否“可迭代”
    for (const member of collection) {
      console.log(member);
    }
  }
}

printAllMembers(staff, departments);

让我们自己实现一个可迭代对象

为了加深理解,我们来创建一个自定义的 Range 类,让它也能被 for...of 遍历。

javascript
class Range {
  constructor(start, end, step = 1) {
    this.start = start;
    this.end = end;
    this.step = step;
  }

  // 关键:实现 [Symbol.iterator] 方法,让 Range 类可迭代
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    const step = this.step;

    // 返回一个迭代器对象
    return {
      next() {
        if (current <= end) {
          const result = { value: current, done: false };
          current += step;
          return result;
        } else {
          return { value: undefined, done: true };
        }
      },
    };
  }
}

// 客户端使用
const myRange = new Range(1, 5);

// 我们的自定义对象现在可以被 for...of 直接使用了!
for (const num of myRange) {
  console.log(num); // 依次输出 1, 2, 3, 4, 5
}

// 也可以被其他接受可迭代对象的原生语法使用
const arrFromRange = [...myRange];
console.log(arrFromRange); // [ 1, 2, 3, 4, 5 ]

通过实现可迭代协议,我们让我们自定义的 Range 类,无缝地融入了 JavaScript 的生态系统,获得了和原生数据结构同等的“待遇”。

四、优缺点与适用场景

优点

  1. 接口统一:提供了一个统一的接口来遍历各种数据结构,极大地简化了客户端代码。
  2. 封装性:隐藏了聚合对象的内部实现细节。
  3. 支持多种遍历方式:可以为一个聚合对象提供多种不同的迭代器(例如,正序遍历、倒序遍历)。
  4. 与语言特性结合:在 JS 中,实现该协议可以让你的对象支持 for...of, 解构赋值 ... 等多种语言特性。

缺点

  1. 增加了复杂性:对于非常简单的数据结构,手动实现迭代器会增加额外的代码。

适用场景

  • 当你需要为一个复杂的数据结构提供一种简单的、统一的访问方式时。
  • 当你需要支持多种遍历方式时。
  • 在 JavaScript 中,当你希望你的自定义集合对象能够像原生集合一样被 for...of 等语法使用时,你就应该实现可迭代协议。