Appearance
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)。
这个协议规定:
- 一个对象如果想成为可迭代的 (Iterable),它必须实现一个键为
[Symbol.iterator]的方法。 - 这个
[Symbol.iterator]方法必须返回一个迭代器对象 (Iterator)。 - 这个迭代器对象必须有一个
next()方法。 next()方法返回一个格式为{ value: '当前值', done: boolean }的对象。done为true时表示遍历结束。
像 Array, String, Map, Set 等原生数据结构,都已经默认实现了这个协议。这正是为什么它们都可以被 for...of 循环统一遍历的原因!
使用 for...of 统一遍历
for...of 循环是可迭代协议的语法糖。它会自动调用对象的 [Symbol.iterator] 方法,获取迭代器,然后不断调用 next() 并取出 value,直到 done 为 true。
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 的生态系统,获得了和原生数据结构同等的“待遇”。
四、优缺点与适用场景
优点
- 接口统一:提供了一个统一的接口来遍历各种数据结构,极大地简化了客户端代码。
- 封装性:隐藏了聚合对象的内部实现细节。
- 支持多种遍历方式:可以为一个聚合对象提供多种不同的迭代器(例如,正序遍历、倒序遍历)。
- 与语言特性结合:在 JS 中,实现该协议可以让你的对象支持
for...of, 解构赋值...等多种语言特性。
缺点
- 增加了复杂性:对于非常简单的数据结构,手动实现迭代器会增加额外的代码。
适用场景
- 当你需要为一个复杂的数据结构提供一种简单的、统一的访问方式时。
- 当你需要支持多种遍历方式时。
- 在 JavaScript 中,当你希望你的自定义集合对象能够像原生集合一样被
for...of等语法使用时,你就应该实现可迭代协议。