掌握JavaScript的遍历哲学:从零实现一个ES6迭代器
你有没有遇到过这种情况——想遍历一个自定义数据结构,却发现for...of不支持?或者在处理大量数据时,内存被中间数组撑爆?又或者面对无限序列(比如用户操作流、传感器数据)时束手无策?
这些看似不同的问题,其实都指向同一个答案:Iterator(迭代器)。
随着 ES6 的到来,JavaScript 终于拥有了原生的、统一的遍历机制。它不只是语法糖,而是一种深刻影响代码设计方式的语言级抽象。今天,我们就来亲手打造一个可迭代结构,彻底搞懂 Iterator 背后的运行逻辑,并理解它为何被称为现代 JS 的“基础设施”。
为什么我们需要统一的遍历方式?
在 ES6 之前,JavaScript 中的遍历方式五花八门:
- 数组用
for (let i = 0; i < arr.length; i++) - 对象属性用
for...in - 类数组对象可能还得借助
Array.prototype.forEach.call() - 自定义集合?对不起,得自己写
.each()方法
这不仅混乱,还带来了几个致命问题:
- 接口不统一:每种类型都要单独处理;
- 无法复用逻辑:过滤、映射等操作难以跨类型通用;
- 性能隐患:像
map().filter()这种链式调用会生成多个中间数组; - 无法表达惰性计算:所有值必须预先准备好。
直到Iterator 协议出现。
现在,只要一个对象能被for...of遍历,它就一定是“可迭代的”。这意味着你可以对数组、字符串、Map、Set,甚至你自己写的类,使用完全相同的语法进行消费。
而这背后的核心,就是两个简单却强大的协议。
可迭代协议 vs 迭代器协议:拆开看看怎么工作的
什么是“可迭代”对象?
一个对象如果实现了[Symbol.iterator]方法,并且该方法返回一个迭代器对象,那它就是“可迭代的”。
const iterable = { [Symbol.iterator]() { // 返回一个符合迭代器协议的对象 return { next() { return { value: 'some value', done: false }; } }; } };当你写下:
for (const item of iterable) { console.log(item); }JS 引擎实际上做了三件事:
- 检查
iterable[Symbol.iterator]是否存在; - 调用它得到一个迭代器实例;
- 不断调用
.next()直到done: true。
就这么简单。
真实世界中的执行流程长什么样?
我们拿数组来验证一下:
const arr = ['a', 'b', 'c']; const it = arr[Symbol.iterator](); console.log(it.next()); // { value: 'a', done: false } console.log(it.next()); // { value: 'b', done: false } console.log(it.next()); // { value: 'c', done: false } console.log(it.next()); // { value: undefined, done: true }看到了吗?每一次.next()调用才产生下一个值。这种“按需生成”的特性,正是惰性求值(Lazy Evaluation)的体现。
这也意味着我们可以轻松表示那些“理论上无限”的序列,比如自然数列、时间戳流、键盘事件……只要你不调用.next(),就不会占用资源。
动手实现:让我们的类支持for...of
光说不练假把式。我们来写一个叫MyList的容器类,让它也能被for...of遍历。
第一步:手动实现迭代器(理解原理)
class MyList { constructor(items = []) { this.items = Array.from(items); } [Symbol.iterator]() { let index = 0; const data = this.items; return { next() { if (index < data.length) { return { value: data[index++], done: false }; } else { return { value: undefined, done: true }; } } }; } }重点解析:
[Symbol.iterator]是语言级别的符号键,避免命名冲突;- 内部通过闭包保存了当前索引
index,实现了状态维护; next()每次只返回一项,做到真正的“一步一步走”。
试试看效果:
const list = new MyList(['hello', 'world']); for (const word of list) { console.log(word); // 输出 hello → world } // 也支持展开运算符! console.log([...list]); // ['hello', 'world']完美。但我们还可以更优雅。
第二步:用生成器函数简化实现(推荐做法)
ES6 提供了一个神器:生成器函数(function*)。它可以自动帮你构造符合迭代器协议的对象。
改写如下:
class MyList { constructor(items = []) { this.items = Array.from(items); } *[Symbol.iterator]() { for (let i = 0; i < this.items.length; i++) { yield this.items[i]; } } }就这么几行,功能完全一样。
yield做了什么?
- 每次遇到
yield,函数暂停并返回值; - 下次调用
.next()时继续执行; - 自动生成
{ value, done }结构; - 不用手动管理状态和边界判断。
小贴士:
yield*还可以委托给其他可迭代对象,例如yield* this.items等价于逐个yield其元素。
这才是我们在实际项目中应该使用的写法:简洁、清晰、不易出错。
更进一步:为同一数据提供多种遍历策略
Iterator 的真正威力在于它的灵活性。你完全可以为同一个数据结构暴露多种遍历方式。
设想这样一个需求:我们希望除了正向遍历外,还能反向读取、只读偶数位、或按条件筛选。
怎么做?
实现多样化的遍历方法
class MyList { constructor(items = []) { this.items = Array.from(items); } // 默认遍历(正向) *[Symbol.iterator]() { yield* this.items; } // 反向遍历 *reverse() { for (let i = this.items.length - 1; i >= 0; i--) { yield this.items[i]; } } // 偶数索引项 *evenIndexed() { for (let i = 0; i < this.items.length; i += 2) { yield this.items[i]; } } // 条件过滤(类似数组 filter,但惰性执行) *filter(predicate) { for (const item of this.items) { if (predicate(item)) yield item; } } }注意这里的模式:
- 所有方法都使用
*定义为生成器; - 外部调用时返回的是一个“可被遍历”的迭代器;
- 并没有立即执行,而是等待消费者驱动。
使用示例:像搭积木一样组合逻辑
const numbers = new MyList([10, 25, 30, 45, 50]); // 反向输出 console.log('反向:'); for (const n of numbers.reverse()) { console.log(n); // 50 → 45 → 30 → 25 → 10 } // 偶数索引项 console.log('偶数索引:'); for (const n of numbers.evenIndexed()) { console.log(n); // 10, 30, 50 } // 只取大于25的值 console.log('大于25的值:'); for (const n of numbers.filter(x => x > 25)) { console.log(n); // 30, 45, 50 }你会发现,这一切都不需要创建新数组。每个yield都是实时计算的结果,内存友好,效率更高。
更重要的是,这种设计体现了关注点分离:数据存储归存储,遍历逻辑归遍历逻辑,互不影响,高度解耦。
实际应用场景:Iterator不只是用来循环
别以为 Iterator 只是为了让你少写几个for循环。它在现代前端架构中有深远影响。
场景一:构建响应式数据流
想象你在做一个实时仪表盘,数据来自 WebSocket 流。你可以把消息队列包装成异步迭代器(AsyncIterator),然后用for await...of消费:
async function* eventStream(socket) { for await (const msg of socket.onMessage()) { yield JSON.parse(msg); } } // 使用 for await (const event of eventStream(ws)) { updateDashboard(event); }这是 RxJS、Svelte Stores、Node.js Streams 等库的思想源头。
场景二:懒加载大数据分页
当你要展示成千上万条记录时,传统做法是一次性拉取全部再渲染,卡顿严重。
而用 Iterator,可以实现“边拉边显”:
async function* paginatedUsers(pageSize = 10) { let page = 1; while (true) { const users = await fetch(`/api/users?page=${page}&size=${pageSize}`); if (users.length === 0) break; for (const user of users) yield user; page++; } } // 在 React 中结合 useAsyncIterator 或自定义 hook 使用每一页只在需要时请求,用户体验流畅。
场景三:函数式编程管道雏形
Iterator + 生成器,天然适合实现链式操作:
function* map(iterable, fn) { for (const item of iterable) yield fn(item); } function* filter(iterable, predicate) { for (const item of iterable) if (predicate(item)) yield item; } function take(iterable, n) { const result = []; for (const item of iterable) { result.push(item); if (result.length >= n) break; } return result; } // 组合使用 const data = [1, 2, 3, 4, 5]; const pipeline = map(filter(data, x => x % 2 === 0), x => x * 2); console.log(take(pipeline, 2)); // [4, 8]整个过程没有中间数组,只有值的流动。这就是所谓的“流式处理”。
使用陷阱与最佳实践
虽然 Iterator 很强大,但也有一些坑需要注意。
⚠️ 陷阱一:迭代器是一次性的
大多数原生迭代器只能消费一次:
const it = [1, 2, 3][Symbol.iterator](); console.log([...it]); // [1, 2, 3] console.log([...it]); // []第二次为空!因为内部状态已经走到尽头。
✅解决方案:
- 每次要遍历时重新获取迭代器:obj[Symbol.iterator]()
- 或者缓存原始数据,在多次遍历时每次都新建迭代器。
⚠️ 陷阱二:不能随机访问
ES6 Iterator 是单向的,不支持it.prev()或it.goto(index)。
如果你需要双向遍历或跳转,要么自己封装状态机,要么考虑使用其他数据结构(如 Immutable List)。
✅ 最佳实践建议
| 建议 | 说明 |
|---|---|
| 优先使用生成器 | 比手动实现更安全、更易读 |
| 保持惰性 | 不要在next()中做昂贵预计算 |
| 做好边界检查 | 防止越界访问引发异常 |
| TypeScript 类型标注 | 使用Iterable<T>和Iterator<T>提升可维护性 |
| 考虑重置能力 | 若需重复消费,提供.values()等方法重新生成 |
写在最后:掌握Iterator,就是掌握现代JS的设计思维
你看,Iterator 表面上只是一个遍历工具,但它背后蕴含的是一种解耦思想:
把“如何访问数据”和“如何处理数据”分开。
这正是函数式编程和面向接口编程的核心理念。
当你学会用 Iterator 去思考问题时,你会开始设计更具弹性的 API:
- 数据源不必一次性加载;
- 处理过程可以流水线化;
- 扩展性变得极强——新增一种遍历方式?加个方法就行;
- 与其他系统集成更容易,因为大家都遵循同样的协议。
未来,随着AsyncIterator、pipeline operator (|>)等提案逐步落地,JavaScript 中的“数据流”将变得更加自然和高效。
所以,下次当你面对复杂的遍历逻辑时,不妨停下来问一句:
“我能把它变成一个可迭代对象吗?”
也许答案就是一把打开新世界大门的钥匙。
如果你正在构建工具库、状态管理器,或是处理大规模数据流,那么深入理解 Iterator,绝对值得投入时间。
毕竟,掌握了遍历的本质,你就掌握了 JavaScript 的“呼吸节奏”。