1. 为什么需要让对象可迭代
在JavaScript中,for...of循环是一种简洁高效的遍历方式,但默认只能用于数组、字符串等内置可迭代对象。当我们尝试对普通对象使用for...of时,会直接抛出"对象不可迭代"的错误。这其实暴露了JavaScript语言设计中的一个重要特性——可迭代协议(Iterable Protocol)。
注意:在ES6之前,我们只能用
for...in遍历对象的键,但这种方式会包含原型链上的属性,且遍历顺序不保证,远不如for...of直观可靠。
让自定义对象支持迭代的核心价值在于:
- 统一数据访问接口:无论是数组、集合还是自定义数据结构,都可以用相同的语法遍历
- 实现惰性计算:迭代器可以按需生成值,节省内存
- 支持所有消费迭代器的语法:包括
...扩展运算符、Array.from()等
2. 可迭代协议与迭代器协议详解
2.1 可迭代协议(Iterable Protocol)
一个对象要成为可迭代对象,必须实现@@iterator方法(即Symbol.iterator属性)。这个方法必须返回一个迭代器对象。当对象被for...of消费时,会自动调用这个方法。
const myIterable = { [Symbol.iterator]: function() { // 返回迭代器对象 } }2.2 迭代器协议(Iterator Protocol)
迭代器对象必须实现next()方法,该方法返回包含两个属性的对象:
value:当前迭代的值done:布尔值,表示迭代是否结束
{ next: function() { return { value: any, // 当前值 done: boolean // 是否结束 } } }3. 实现自定义可迭代对象
3.1 基础实现示例
让我们创建一个简单的范围迭代器,生成从start到end的数字序列:
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 { done: true } } } } } } // 使用示例 for (const num of new Range(1, 5)) { console.log(num) // 依次输出1,2,3,4,5 }3.2 更复杂的树形结构迭代
对于复杂数据结构,迭代器可以按特定顺序遍历。以下是一个二叉树的中序遍历实现:
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 } } // 构建树 const tree = new TreeNode(4, new TreeNode(2, new TreeNode(1), new TreeNode(3) ), new TreeNode(5) ) // 中序遍历 for (const val of tree) { console.log(val) // 输出1,2,3,4,5 }技巧:使用生成器函数(function*)可以大大简化迭代器实现,它会自动返回符合迭代器协议的对象。
4. 高级迭代技巧与应用场景
4.1 无限序列迭代器
迭代器不一定要有终点,可以实现无限序列:
function fibonacci() { let [prev, curr] = [0, 1] return { [Symbol.iterator]() { return this }, next() { [prev, curr] = [curr, prev + curr] return { value: curr, done: false } } } } // 使用take限制数量 function* take(iterable, n) { for (const item of iterable) { if (n-- <= 0) return yield item } } // 获取前10个斐波那契数 for (const num of take(fibonacci(), 10)) { console.log(num) }4.2 异步迭代器(ES2018)
对于异步数据源,可以实现异步迭代协议:
const asyncIterable = { [Symbol.asyncIterator]() { let i = 0 return { next() { if (i < 5) { return Promise.resolve({ value: i++, done: false }) } return Promise.resolve({ done: true }) } } } } ;(async function() { for await (const item of asyncIterable) { console.log(item) // 0,1,2,3,4 } })()5. 常见问题与解决方案
5.1 迭代器复用问题
同一个迭代器通常只能遍历一次,再次遍历需要重新获取迭代器:
const range = new Range(1, 3) const it = range[Symbol.iterator]() console.log(it.next().value) // 1 console.log(it.next().value) // 2 // 再次使用同一个迭代器 for (const num of it) { console.log(num) // 只输出3,因为前两个值已经被消费 } // 正确做法是每次for...of都获取新迭代器 for (const num of range) { console.log(num) // 1,2,3 }5.2 手动调用迭代器
有时需要手动控制迭代过程:
const iterable = { data: [10, 20, 30], [Symbol.iterator]() { let index = 0 return { next: () => { if (index < this.data.length) { return { value: this.data[index++], done: false } } return { done: true } } } } } const iterator = iterable[Symbol.iterator]() let result = iterator.next() while (!result.done) { console.log(result.value) result = iterator.next() }5.3 与生成器函数的配合
生成器函数会自动返回符合迭代器协议的对象,极大简化实现:
const obj = { *[Symbol.iterator]() { yield 1 yield 2 yield 3 } } for (const num of obj) { console.log(num) // 1,2,3 }6. 实际应用场景
6.1 自定义集合类
实现类似Java的集合类迭代:
class Collection { constructor() { this.items = [] } add(item) { this.items.push(item) return this } *[Symbol.iterator]() { for (const item of this.items) { yield item } } } const collection = new Collection() collection.add('a').add('b').add('c') for (const item of collection) { console.log(item) // a,b,c }6.2 分页数据获取
模拟API分页数据的迭代:
function createPager(total, pageSize = 10) { let currentPage = 0 const totalPages = Math.ceil(total / pageSize) return { [Symbol.iterator]() { return { next() { if (currentPage < totalPages) { // 模拟API请求 const start = currentPage * pageSize const end = start + pageSize const data = Array.from({length: pageSize}, (_, i) => start + i + 1) currentPage++ return { value: data, done: false } } return { done: true } } } } } } for (const page of createPager(35, 10)) { console.log('Page:', page) // Page: [1,2,...,10] // Page: [11,12,...,20] // Page: [21,22,...,30] // Page: [31,32,...,35] }6.3 状态机迭代
用迭代器实现状态机:
function* stateMachine() { let state = 'start' while (true) { switch (state) { case 'start': console.log('Starting...') state = 'running' yield break case 'running': console.log('Running...') state = 'paused' yield break case 'paused': console.log('Paused...') state = 'stopped' yield break case 'stopped': console.log('Stopped.') return } } } const machine = stateMachine() machine.next() // Starting... machine.next() // Running... machine.next() // Paused... machine.next() // Stopped.7. 性能考量与最佳实践
7.1 迭代器性能
虽然迭代器提供了优雅的抽象,但在性能关键路径上需要注意:
- 每次迭代都会创建新的迭代器对象
yield操作有一定开销- 在V8引擎中,手写迭代器比生成器函数性能稍好
7.2 内存管理
对于大型数据集:
- 考虑惰性计算,不要预先生成所有数据
- 及时释放不再需要的迭代器引用
- 对于无限迭代器,确保有终止条件
7.3 最佳实践总结
- 优先使用生成器函数简化实现
- 为迭代器添加
return()和throw()方法实现完整协议 - 考虑实现
[Symbol.asyncIterator]支持异步场景 - 文档中明确说明对象的迭代顺序和规则
- 对于性能敏感场景,提供备用的直接访问方法
在实现自定义迭代时,我发现最常遇到的坑是忘记迭代器是有状态的。一个常见的错误是在多个地方复用同一个迭代器实例,导致意外的行为。正确的做法是每次迭代都通过[Symbol.iterator]()获取新的迭代器。