🤔

迭代器模式(Iterator)

1)从一个需求/问题引入:为什么我不想再写一堆 for 了?

假设你在做一个列表组件,需要支持遍历不同类型的数据源:

  • 普通数组:[{...}, {...}]
  • 类数组(argumentsNodeList
  • 自定义数据结构:比如分页加载的结果、树节点的“下一项”、甚至是后端流式返回的数据

你很快会写出很多“遍历细节”:

  • 有的用 for (let i=0; i<arr.length; i++)
  • 有的要把 NodeListArray.from(...)
  • 有的还得处理“下一项在哪里”的逻辑

痛点:业务只想要“依次拿到元素”,不想关心“内部怎么存、怎么走”。
这就是迭代器模式要解决的问题:把遍历逻辑从使用方剥离出来,并统一访问方式


2)迭代器模式的关键是什么?

迭代器模式(Iterator Pattern)的核心要点可以概括为三句:

  1. 提供一种顺序访问聚合对象元素的方法
  2. 不暴露聚合对象的内部表示(不让外部依赖内部结构)
  3. 将遍历算法与容器结构解耦(遍历规则可替换、可复用)

在 JavaScript 里,这套思想最常见的落地是 可迭代协议

  • Iterable:对象有 Symbol.iterator 方法,返回一个迭代器
  • Iterator:迭代器有 next() 方法,返回形如 { value, done }

3)用一个简单代码例子说明(手写一个可迭代对象)

下面实现一个“范围对象”range(1, 3),它不是数组,但可以像数组一样被遍历:

function range(start, end) {
  return {
    [Symbol.iterator]() {
      let current = start;
      return {
        next() {
          if (current <= end) {
            return { value: current++, done: false };
          }
          return { value: undefined, done: true };
        }
      };
    }
  };
}

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

你得到的收益

  • 使用方只写 for...of(或者 ...spreadArray.from
  • range 内部怎么计算“下一项”完全隐藏
  • 将来你可以换成倒序、步长、过滤等策略,而外部遍历代码不变

再看一下迭代器在 JS 生态中的“通用性”:

const nums = [...range(5, 7)];     // [5, 6, 7]
const arr = Array.from(range(2, 4)); // [2, 3, 4]

4)常见应用场景(前端非常实用)

场景 A:统一遍历不同数据源

你可能同时接收数组、Set、Map、NodeList、自定义集合。只要它们是 Iterable,就能统一写法:

function consume(iterable) {
  for (const item of iterable) {
    // 统一处理
  }
}

consume(new Set([1, 2, 3]));
consume(document.querySelectorAll("div")); // NodeList(多数现代环境可迭代)

场景 B:惰性计算 / 大数据遍历(按需生成)

range 这种“边走边算”的迭代器,不需要一次性生成巨大的数组,节省内存。

场景 C:封装复杂遍历规则(树、图、分页)

例如树结构的 DFS/BFS 遍历、分页接口自动拉取下一页,都可以用迭代器把“拿下一个元素”的策略封装起来,让业务层只管 for...of

场景 D:自定义集合与业务 API 解耦

你的组件/函数对外暴露 Iterable,而不是暴露内部数组、链表、游标等结构,实现更稳定的 API。


5)注意事项(避免踩坑)

  1. 迭代器通常是“一次性消费”的
    很多迭代器(尤其是生成器、流式数据)遍历完就没了;如果需要复用,应该让对象本身可迭代(每次 Symbol.iterator() 产生新迭代器),而不是复用同一个迭代器实例。

  2. 不要在遍历过程中随意修改底层集合
    比如遍历数组时 push/splice,可能导致漏元素、重复、甚至死循环。需要修改时,考虑先复制快照或定义明确的规则。

  3. 注意可迭代 vs 类数组的差别
    类数组(有 length 和索引)不一定可迭代;可迭代也不一定有索引。优先用 for...of 面向 Iterable。

  4. 异步数据别硬用同步迭代器
    如果“下一个元素”需要异步获取,应该用 异步迭代器Symbol.asyncIterator + for await...of),不要在 next() 里塞异步但又返回同步结果。

  5. 保持迭代协议一致性 next() 必须返回形如 { value, done } 的对象;done: true 时表示结束,避免让外部遍历出现不可预期行为。


小结

迭代器模式在前端的价值是:让“遍历”成为一种稳定的协议
你把“怎么取下一个元素”的复杂性关在迭代器内部,业务层只保留简洁一致的消费方式(for...of / Array.from / 展开运算符等),从而提升可维护性和可扩展性。