zhangdizhangdi

迭代器模式 ⭐

定义

提供一种方法顺序访问一个聚合对象(如数组、链表、树等)中的各个元素,而又不需要暴露该对象的内部结构

就像你听歌时的音乐播放器。不管你的歌单底层是用什么存储的(是本地硬盘上的文件、还是云端的流媒体、是用数组存的还是用链表存的),你都不需要关心。你只需要按 “下一首(Next)”,它就会自动给你播放下一首歌。

不同的数据结构(数组、Set、Map、树形图)有不同的遍历方式。如果让业务代码去适配每一种结构,代码会变得极其臃肿。
迭代器模式通过提供统一的接口(如 next()),屏蔽了底层数据结构的差异

与设计原则的关系

  1. 单一职责原则 (Single Responsibility Principle)
    • 最核心的体现。一个集合对象(比如数组类)的职责应该是**“存储数据”,而不是“遍历数据”**。迭代器模式将“遍历逻辑”从集合对象中抽离出来,交给了专门的迭代器去负责。
  2. 开闭原则 (Open-Closed Principle)
    • 你可以轻松地实现新的遍历算法。比如你想要一个“倒序迭代器”、“只遍历偶数的迭代器”或者“深度优先遍历树的迭代器”,你只需要写一个新的迭代器类即可,完全不用修改原有的集合代码。

实现

1. 传统的 OOP

在没有语言内置支持时,我们需要手动维护索引指针(index)和两个核心方法:hasNext()next()

javascript
// 传统迭代器类
class ArrayIterator {
  constructor(collection) {
    this.collection = collection;
    this.index = 0; // 内部游标
  }

  // 是否还有下一个元素
  hasNext() {
    return this.index < this.collection.length;
  }

  // 返回当前元素,并把游标往下走一步
  next() {
    if (this.hasNext()) {
      return this.collection[this.index++];
    }
    return null;
  }
}

// 使用
const names =['张三', '李四', '王五'];
const iterator = new ArrayIterator(names);

while (iterator.hasNext()) {
  console.log(iterator.next()); 
  // 依次输出:张三, 李四, 王五
}

2. ES6

ECMAScript 2015 (ES6) 规定了统一的迭代器协议(Iterator Protocol)可迭代协议(Iterable Protocol)
在 JS 中,迭代器不再用 hasNext,而是统一返回一个形如 { value: Any, done: Boolean } 的对象。

我们可以通过给对象部署 Symbol.iterator 方法,让任何普通对象变成“可遍历”的。

javascript
// 假设我们有一个公司的组织架构对象,它不是数组,本来是不能被遍历的
const company = {
  employees:['Alice', 'Bob', 'Charlie'],
  
  // 部署 Symbol.iterator 使其成为“可迭代对象”[Symbol.iterator]: function() {
    let index = 0;
    const items = this.employees;
    
    // 必须返回一个迭代器对象(含有 next 方法)
    return {
      next: function() {
        if (index < items.length) {
          return { value: items[index++], done: false };
        } else {
          return { value: undefined, done: true }; // 遍历结束
        }
      }
    };
  }
};

// 魔法出现了!由于部署了迭代器,现在可以直接用 for...of 遍历这个普通对象了
for (const employee of company) {
  console.log(employee); 
  // 依次输出: Alice, Bob, Charlie
}

前端应用

1. JS 内置迭代器

你平时写代码时使用的很多原生对象,底层都已经自动实现了迭代器(自带 [Symbol.iterator]):

  • Array(数组)
  • String(字符串可以逐个字符遍历)
  • MapSet
  • 函数的 arguments 对象
  • DOM NodeList(比如 document.querySelectorAll('div') 选出来的元素列表)

2. 各种“语法糖”的底层原理

为什么这些结构长得完全不同,却都可以使用统一的语法去操作?因为它们背后调用的都是迭代器模式

以下语法糖,本质上都在悄悄调用对象的 next() 方法

  • for...of 循环
  • 展开运算符(Spread Operator): [...new Set([1,2,2,3])] 为什么能转成数组?因为它调用了 Set 的迭代器。
  • 解构赋值: const[a, b] = new Set([1, 2])
  • Array.from()
  • Promise.all(iterable) (接收的参数是一个可迭代对象)

3. Generator(生成器)

写一个包含 next 方法的对象有时太繁琐了,ES6 提供了 function*(生成器函数)来更优雅地创建迭代器。

javascript
// 用生成器改写上面的公司员工遍历:极其优雅
const company = {
  employees:['Alice', 'Bob', 'Charlie'],
  *[Symbol.iterator]() {
    for (let emp of this.employees) {
      yield emp; // 每次 next() 走到这里暂停,并返回数据
    }
  }
};

4. Redux-Saga 处理异步流

在 React 生态著名的异步状态管理库 redux-saga 中,彻底贯彻了迭代器模式。
Saga 利用 Generator 函数交出代码控制权。Saga 中间件扮演了“执行者”的角色,它不断地调用 iterator.next(),拿到异步操作(如发请求)的描述信息,等请求成功后,再调用 next(结果) 把值塞回给 Generator。将极其复杂的异步回调变成了同步写法的代码。

5. DOM 树的遍历 (TreeWalker / NodeIterator)

浏览器提供了一套非常冷门但纯正的迭代器 API 用于遍历 DOM 树,屏蔽了深度优先/广度优先算法的复杂性:

javascript
const iterator = document.createNodeIterator(
  document.body, 
  NodeFilter.SHOW_TEXT // 只遍历文本节点!不需要你自己去写递归了
);

let node;
while ((node = iterator.nextNode())) {
  console.log(node.nodeValue);
}

总结

迭代器模式的核心在于“统一封装与解耦”。前端开发者平时几乎不需要像 Java 开发者那样手动从零手写迭代器类,因为语言标准(ES6 Iterator)和生成器(Generator)已经把这个模式升华为了语言内置的基础设施。理解迭代器模式,是你深入理解 JS 高级语法(如 for...of、解构、展开语法、异步编程本质)的唯一钥匙。