在JavaScript开发中,我们经常需要遍历各种数据结构,但不同的结构(如数组、树、图、自定义集合)其内部组织方式差异很大。如果每次都在业务代码里写死遍历过程,不仅冗余,而且难以维护。迭代器模式的核心思想,就是将“如何遍历”这件事从数据结构本身剥离出来,交给一个独立的迭代器对象负责。调用方只需要不断向迭代器索取下一个元素,而不必知道数据藏在哪、怎么走。

什么是迭代器模式
迭代器模式是一种行为设计模式,它提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。在JavaScript中,迭代器是一个对象,必须实现next()方法。每次调用next()都会返回一个结果对象,其中包含两个属性:value表示当前元素的值,done为布尔值,指示是否已经遍历完毕。
这种设计的优势在于,遍历逻辑被封装在迭代器内部。比如一个二叉树结构,可以提供一个中序遍历的迭代器,也可以提供一个层序遍历的迭代器,而使用者的代码完全不用变。它让算法与数据结构解耦,也支持惰性计算——元素只有在被请求时才生成,这对处理海量或无限序列尤其重要。
原生JavaScript中的迭代器协议
JavaScript从ES6开始正式支持迭代器协议和可迭代协议。可迭代协议要求对象实现一个名为Symbol.iterator的方法,该方法返回一个迭代器。内置的数组、字符串、Map、Set都已经实现了该协议,因此可以直接用于for...of循环。
下面用一个简单例子展示最基本的迭代器实现。我们手动创建一个返回固定序列的迭代器,帮助理解底层机制:
// 一个返回 1,2,3 的简易迭代器
function createSimpleIterator() {
let index = 0;
const data = [1, 2, 3];
return {
next: function() {
if (index < data.length) {
return { value: data[index++], done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
const it = createSimpleIterator();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }
上面代码中,next函数通过闭包维持index状态,每次调用推进一次。当超出长度后,done变为true。这就是自定义遍历逻辑的最小原型。
自定义可迭代对象与Symbol.iterator
如果希望自己的数据结构能用在for...of中,或者能被扩展运算符展开,就需要让它成为可迭代对象。做法是在对象上添加Symbol.iterator方法,并返回符合迭代器协议的对象。
我们来实现一个“跳过空值”的数组包装器。普通数组遍历会遇到null、undefined或空字符串,很多时候业务希望直接忽略它们。把这套过滤逻辑写在迭代器里,外部遍历代码就干净了:
class SkipEmptyArray {
constructor(arr) {
this.arr = arr;
}
[Symbol.iterator]() {
let index = 0;
const arr = this.arr;
return {
next: function() {
// 向前查找下一个非空值
while (index < arr.length) {
const val = arr[index++];
if (val !== null && val !== undefined && val !== '') {
return { value: val, done: false };
}
}
return { value: undefined, done: true };
}
};
}
}
const list = new SkipEmptyArray([1, null, 2, '', 3, undefined]);
for (const item of list) {
console.log(item); // 依次输出 1 2 3
}
const spreaded = [...new SkipEmptyArray([10, null, 20])];
console.log(spreaded); // [10, 20]
这个例子展示了自定义遍历逻辑的实用性:遍历规则(跳过空值)被完全封装,调用方使用标准语法即可。如果以后规则变为“只取偶数”,只需修改迭代器内部,外部代码零改动。
用生成器简化迭代器编写
手动写next和状态变量容易出错。JavaScript的function*生成器函数能自动产生迭代器,用yield返回值,状态由引擎托管。上面的跳过空值逻辑用生成器重写会更直观:
function* skipEmptyGenerator(arr) {
for (const item of arr) {
if (item !== null && item !== undefined && item !== '') {
yield item;
}
}
}
const gen = skipEmptyGenerator([5, null, 6, '', 7]);
console.log(gen.next().value); // 5
console.log(gen.next().value); // 6
console.log([...skipEmptyGenerator([0, false, 8])]); // [0, false, 8] 注意 0 和 false 不被跳过
生成器本质上是语法糖,但它让自定义遍历逻辑的表达成本大幅降低。你可以用循环、条件、甚至嵌套生成器来描述复杂遍历顺序,比如先序走树、按权重出队等。
在树结构中应用自定义遍历
迭代器模式在非线性结构里价值最大。假设我们有一个简单的树节点,希望外部用统一方式拿到所有节点,但不想暴露递归细节。下面给出一个深度优先的可迭代树:
class TreeNode {
constructor(value) {
this.value = value;
this.children = [];
}
add(child) {
this.children.push(child);
}
[Symbol.iterator]() {
const stack = [this];
return {
next: () => {
if (stack.length === 0) {
return { value: undefined, done: true };
}
const node = stack.pop();
// 反向压栈以保证从左到右遍历
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
return { value: node.value, done: false };
}
};
}
}
const root = new TreeNode('root');
const a = new TreeNode('a');
const b = new TreeNode('b');
root.add(a);
root.add(b);
a.add(new TreeNode('a1'));
for (const v of root) {
console.log(v); // root a b a1
}
这里用显式栈代替递归,迭代器每次弹出一个节点并把子节点压回。调用方完全感知不到树的存在,只拿到一条线性流。若改为广度优先,只需把栈换成队列,外部循环一行都不用改。
自定义迭代器的注意事项
实现迭代器时要注意状态隔离。如果同一个可迭代对象被多个for...of同时使用,每次调用Symbol.iterator都应返回全新的迭代器实例,否则会相互干扰。另外,迭代器一旦写完,建议保持“单向移动”语义,不要提供回退,这符合大多数语言的习惯,也降低复杂度。
性能方面,自定义迭代器通常比内置循环稍慢一点,因为多了函数调用和对象分配,但在绝大多数业务场景可以忽略。真正收益是代码清晰与复用:遍历算法只写一次,任何地方都能接for...of、Array.from、解构等语法。当数据结构演进时,只要迭代器契约不变,系统其余部分就是稳定的。
总结
JavaScript迭代器模式通过next方法与Symbol.iterator,让开发者能把任意遍历逻辑包装成标准协议。无论是过滤空值、控制步长、惰性拉取,还是遍历树与图,都可以用同一套消费方式处理。借助生成器,编写成本进一步下降。掌握它,能让你的数据结构和业务代码彻底解耦,写出更易于测试和扩展的程序。
JavaScript迭代器模式自定义遍历修改时间:2026-08-09 07:03:34