迭代器模式 ⭐
定义
提供一种方法顺序访问一个聚合对象(如数组、链表、树等)中的各个元素,而又不需要暴露该对象的内部结构。
就像你听歌时的音乐播放器。不管你的歌单底层是用什么存储的(是本地硬盘上的文件、还是云端的流媒体、是用数组存的还是用链表存的),你都不需要关心。你只需要按 “下一首(Next)”,它就会自动给你播放下一首歌。
不同的数据结构(数组、Set、Map、树形图)有不同的遍历方式。如果让业务代码去适配每一种结构,代码会变得极其臃肿。
迭代器模式通过提供统一的接口(如 next()),屏蔽了底层数据结构的差异。
与设计原则的关系
- 单一职责原则 (Single Responsibility Principle):
- 最核心的体现。一个集合对象(比如数组类)的职责应该是**“存储数据”,而不是“遍历数据”**。迭代器模式将“遍历逻辑”从集合对象中抽离出来,交给了专门的迭代器去负责。
- 开闭原则 (Open-Closed Principle):
- 你可以轻松地实现新的遍历算法。比如你想要一个“倒序迭代器”、“只遍历偶数的迭代器”或者“深度优先遍历树的迭代器”,你只需要写一个新的迭代器类即可,完全不用修改原有的集合代码。
实现
1. 传统的 OOP
在没有语言内置支持时,我们需要手动维护索引指针(index)和两个核心方法:hasNext() 和 next()。
// 传统迭代器类
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 方法,让任何普通对象变成“可遍历”的。
// 假设我们有一个公司的组织架构对象,它不是数组,本来是不能被遍历的
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(字符串可以逐个字符遍历)Map和Set- 函数的
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*(生成器函数)来更优雅地创建迭代器。
// 用生成器改写上面的公司员工遍历:极其优雅
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 树,屏蔽了深度优先/广度优先算法的复杂性:
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、解构、展开语法、异步编程本质)的唯一钥匙。