从“如何遍历”到“能被遍历”:深入理解 ES6 中的 Iterator 机制
你有没有想过,为什么 JavaScript 中的数组可以用for...of遍历,字符串也可以,连Set和Map甚至函数内的arguments都能轻松地被展开或循环?而你自己写的一个对象却不行?
const arr = [1, 2, 3]; for (const item of arr) { console.log(item); // 正常输出 1, 2, 3 } const obj = { a: 1, b: 2 }; // for (const item of obj) // 报错!obj is not iterable这背后其实有一套统一的规则在起作用——这就是ES6 引入的 Iterator(遍历器)机制。它不是某个具体的数据结构,而是一种“行为协议”,决定了一个东西“能不能被遍历”以及“怎么被遍历”。
一、问题驱动:为什么需要 Iterator?
在 ES6 之前,JavaScript 的遍历方式五花八门:
- 数组用
for (let i = 0; i < arr.length; i++) - 对象属性用
for...in - 类数组对象(如
arguments)还得借用Array.prototype.forEach.call() - 自定义结构?抱歉,得自己实现一套逻辑
这些方式各有各的语义和限制,缺乏统一性。更麻烦的是,如果你想让别人用熟悉的语法来操作你的数据类型(比如一棵树、一个懒加载队列),就必须手动模拟整个流程。
于是,ES6 提出了一个核心思想:
把“遍历的能力”从数据结构中剥离出来,变成一种可复用的标准接口。
这个接口就是Iterator 模式。
二、Iterator 是什么?一个简单的协议
我们先不谈复杂概念,来看最本质的一点:Iterator 其实就是一个有next()方法的对象。
每次调用next(),返回一个形如{ value: ..., done: boolean }的结果。
value:当前拿到的值;done:是否已经遍历完了。
就这么简单。
动手实现一个计数器迭代器
function createCounter(max) { let current = 0; return { next() { if (current < max) { return { value: current++, done: false }; } else { return { value: undefined, done: true }; } } }; }使用它:
const counter = createCounter(3); console.log(counter.next()); // { value: 0, done: false } console.log(counter.next()); // { value: 1, done: false } console.log(counter.next()); // { value: 2, done: false } console.log(counter.next()); // { value: undefined, done: true }看到没?这个对象就是一个标准的iterator。它符合规范,能一步步往外“吐”值,直到结束。
但注意:你现在还不能对它使用for...of—— 因为它只是个 iterator,还不是“可迭代对象”。
三、可迭代协议:让对象“能被遍历”
刚才那个counter虽然本身是 iterator,但它不能直接放进for...of里用。因为 JS 不知道“怎么开始遍历它”。
要解决这个问题,就需要另一个协议:可迭代协议(Iterable Protocol)。
它的规则只有一条:
如果一个对象有一个方法,名叫
Symbol.iterator,并且这个方法返回一个 iterator,那它就是“可迭代的”。
也就是说,“可迭代对象” ≠ “迭代器”,而是“能生成迭代器的东西”。
让 Range 成为可迭代对象
举个例子,我们做一个范围类Range(1, 5),表示从 1 到 4 的整数序列。
class Range { constructor(start, end) { this.start = start; this.end = end; } [Symbol.iterator]() { let current = this.start; const end = this.end; return { next() { if (current < end) { return { value: current++, done: false }; } else { return { value: undefined, done: true }; } } }; } }现在试试看:
const range = new Range(1, 5); for (const n of range) { console.log(n); // 输出 1, 2, 3, 4 } const arr = [...range]; // [1, 2, 3, 4]成功了!Range现在可以像数组一样被for...of和扩展运算符使用。
关键就在于这一行:
[Symbol.iterator]() { ... }这是 JS 的“魔法入口”。当你写for...of someObj时,引擎会自动查找someObj[Symbol.iterator]()并调用它,拿到 iterator 后再反复执行next()。
四、Generator:让创建迭代器变得极其简单
上面的例子虽然清晰,但代码有点啰嗦。尤其是状态管理(current变量)容易出错。
ES6 还提供了一个更强大的工具:Generator 函数。
它本质上是一个“能暂停的函数”,配合yield使用,天然返回一个符合 Iterator 接口的对象。
用 Generator 改写 ID 生成器
function* idGenerator() { let id = 1; while (true) { yield id++; } }调用它:
const gen = idGenerator(); gen.next(); // { value: 1, done: false } gen.next(); // { value: 2, done: false }而且它自己就实现了Symbol.iterator:
gen[Symbol.iterator]() === gen; // true → 它自己就是可迭代的所以可以直接用于for...of:
for (const id of idGenerator()) { if (id > 5) break; console.log('ID:', id); // 输出 1 到 5 }是不是简洁太多了?不需要手动维护next()和状态判断,yield就像一个“产出点”,每走到这里就暂停并返回值。
五、实际应用场景:Iterator 不只是用来循环
别以为 Iterator 只是为了替代for循环。它的真正威力在于抽象遍历过程,适用于很多高级场景。
场景 1:遍历树结构(中序遍历)
假设你有一棵二叉树,想让用户用for...of直接遍历所有节点值。
class TreeNode { constructor(value, left = null, right = null) { this.value = value; this.left = left; this.right = right; } *[Symbol.iterator]() { if (this.left) yield* this.left; yield this.value; if (this.right) yield* this.right; } }解释一下:
yield*表示“委托给另一个可迭代对象”;- 左子树如果是
TreeNode,也实现了[Symbol.iterator],就能递归展开; - 最终效果是按中序顺序依次产出每个节点的值。
使用起来就像普通数组一样自然:
const root = new TreeNode( 2, new TreeNode(1), new TreeNode(3) ); for (const val of root) { console.log(val); // 输出 1, 2, 3 }完全隐藏了内部结构的复杂性。
场景 2:处理大数据流或懒加载资源
想象你要读取一个超大文件,或者分页获取远程数据。如果一次性加载进内存,可能会崩溃。
这时可以用 Iterator 实现“按需加载”:
function* paginatedUsers(pageSize = 10) { let page = 1; while (true) { const response = await fetch(`/api/users?page=${page}&size=${pageSize}`); const users = await response.json(); if (users.length === 0) break; for (const user of users) { yield user; // 每次只返回一个用户 } page++; } }等等……这不行!Generator 函数内部不能用await!
别急,ES2018 提供了异步迭代器(Async Iterator),支持for await...of。
改写如下:
async function* asyncPaginatedUsers(pageSize = 10) { let page = 1; while (true) { const response = await fetch(`/api/users?page=${page}&size=${pageSize}`); const users = await response.json(); if (users.length === 0) break; for (const user of users) { yield user; } page++; } }然后这样使用:
for await (const user of asyncPaginatedUsers()) { console.log(user.name); }这就是现代前端处理无限滚动、日志流、WebSocket 数据的基础模型之一。
六、设计建议与常见坑点
✅ 好实践
每次
[Symbol.iterator]()应返回新实例js [Symbol.iterator]() { let current = this.start; return { /* 返回新的 next 函数 */ }; }
避免多个遍历共享状态导致混乱。无限迭代器记得加退出条件
js for (const x of infiniteGen()) { if (x > 100) break; // 必须手动中断 }优先使用 Generator 创建 iterator
写法更清晰,不易出错,还能结合try...catch处理异常。
❌ 常见误区
| 错误做法 | 问题说明 |
|---|---|
在普通对象上直接加next() | 它不是可迭代对象,无法用于for...of |
Symbol.iterator返回固定对象 | 多次遍历时状态共享,第二次可能为空 |
Generator 中混用yield和return不当 | return会提前终结迭代 |
七、总结:Iterator 到底带来了什么?
回到最初的问题:Iterator 的意义是什么?
我们可以这样理解:
| 以前 | 现在 |
|---|---|
| 数据结构决定怎么遍历 | 遍历方式由协议决定 |
| 每种结构各搞一套 API | 所有结构共用一套语法 |
| 用户关心“怎么取下一个” | 用户只关心“拿到下一个” |
Iterator 把“如何遍历”的细节封装起来,对外暴露统一的消费接口。
它是现代 JavaScript 中许多特性的基石:
for...of- 扩展运算符
... - 解构赋值
[a, b, ...rest] = iterable Array.from(),Promise.all(iterable)yield*for await...of- Redux-Saga、RxJS 等库的核心模型
掌握 Iterator,不只是学会一种语法,更是理解 JS 如何通过协议 + 语法糖构建出强大而灵活的抽象能力。当你下次想封装一个自定义集合、实现懒加载、或是设计一个状态机时,不妨问一句:
“这个东西,能让别人用
for...of来遍历吗?”
如果答案是肯定的,那你已经走在写出更优雅代码的路上了。
如果你在项目中用过 Iterator 解决实际问题,欢迎在评论区分享你的经验!