迭代器模式(Iterator)
1)从一个需求/问题引入:为什么我不想再写一堆 for 了?
假设你在做一个列表组件,需要支持遍历不同类型的数据源:
- 普通数组:
[{...}, {...}] - 类数组(
arguments、NodeList) - 自定义数据结构:比如分页加载的结果、树节点的“下一项”、甚至是后端流式返回的数据
你很快会写出很多“遍历细节”:
- 有的用
for (let i=0; i<arr.length; i++) - 有的要把
NodeList先Array.from(...) - 有的还得处理“下一项在哪里”的逻辑
痛点:业务只想要“依次拿到元素”,不想关心“内部怎么存、怎么走”。
这就是迭代器模式要解决的问题:把遍历逻辑从使用方剥离出来,并统一访问方式。
2)迭代器模式的关键是什么?
迭代器模式(Iterator Pattern)的核心要点可以概括为三句:
- 提供一种顺序访问聚合对象元素的方法
- 不暴露聚合对象的内部表示(不让外部依赖内部结构)
- 将遍历算法与容器结构解耦(遍历规则可替换、可复用)
在 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(或者...spread、Array.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)注意事项(避免踩坑)
-
迭代器通常是“一次性消费”的
很多迭代器(尤其是生成器、流式数据)遍历完就没了;如果需要复用,应该让对象本身可迭代(每次Symbol.iterator()产生新迭代器),而不是复用同一个迭代器实例。 -
不要在遍历过程中随意修改底层集合
比如遍历数组时push/splice,可能导致漏元素、重复、甚至死循环。需要修改时,考虑先复制快照或定义明确的规则。 -
注意可迭代 vs 类数组的差别
类数组(有length和索引)不一定可迭代;可迭代也不一定有索引。优先用for...of面向 Iterable。 -
异步数据别硬用同步迭代器
如果“下一个元素”需要异步获取,应该用 异步迭代器(Symbol.asyncIterator+for await...of),不要在next()里塞异步但又返回同步结果。 -
保持迭代协议一致性
next()必须返回形如{ value, done }的对象;done: true时表示结束,避免让外部遍历出现不可预期行为。
小结
迭代器模式在前端的价值是:让“遍历”成为一种稳定的协议。
你把“怎么取下一个元素”的复杂性关在迭代器内部,业务层只保留简洁一致的消费方式(for...of / Array.from / 展开运算符等),从而提升可维护性和可扩展性。