先记住这个答案
普通对象要支持 for...of 和展开,需要实现迭代协议:给对象定义一个 [Symbol.iterator]() 方法,它返回一个迭代器对象;迭代器上有 next() 方法,每次调用返回 { value, done },其中 done 为 false 时 value 是下一项,done 为 true 表示结束。for...of 做的事情就是先调用 [Symbol.iterator]() 拿到迭代器,再反复调用 next(),直到 done 为 true。要让对象能被反复遍历,游标必须在每次调用 [Symbol.iterator]() 时新建,而不是存在对象上共用。遍历中途 break 时,还会调用迭代器可选的 return() 方法,可以在里面释放资源。
- 可迭代对象有
[Symbol.iterator](),它返回迭代器 - 迭代器有
next(),每次返回{ value, done } - 每次调用
Symbol.iterator都新建游标,才能反复遍历 break提前退出时会调用迭代器的return()
手写一个可以反复遍历的范围对象
这里有两个角色要分清:可迭代对象是「能被遍历的东西」,迭代器是「正在遍历的那个游标」。
nextJavaScriptconst range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let cur = this.from // 每次调用都新建一个游标
const last = this.to
return {
next() {
return cur <= last ? { value: cur++, done: false } : { value: undefined, done: true }
}
}
}
}
console.log(JSON.stringify([...range]))
console.log(JSON.stringify([...range])) // 第二次展开照样有数据
for (const n of range) console.log(n)查看输出与解释
[1,2,3]
[1,2,3]
1
2
3cur 是 [Symbol.iterator]() 里的局部变量,每次展开或 for...of 都会重新调用这个方法,得到一个从 1 开始的新游标。所以 range 可以被遍历任意多次,多个 for...of 嵌套遍历同一个 range 也互不干扰。
容易答错的地方
- 把
Symbol.iterator写成字符串键 { 'Symbol.iterator'() {} }定义的是一个名叫"Symbol.iterator"的普通字符串属性,for...of不认。必须用计算属性写成[Symbol.iterator]()。next返回的不是对象next()必须返回一个对象,如果直接返回值或者undefined,执行到for...of时就会抛TypeError。结束时也要返回{ done: true },这时value可以省略。- 以为实现了迭代器就是数组了
- 对象能被
for...of、展开、Array.from使用,但它没有length,也没有map、filter,用Array.isArray判断也是false。需要数组方法时先[...obj]转一下。
面试官还会怎么问?
用生成器怎么写同样的功能?
把方法写成生成器:*[Symbol.iterator]() { for (let i = this.from; i <= this.to; i++) yield i }。生成器函数每次调用都返回一个新的生成器对象,它天然就是迭代器,也实现了 return(),所以可以反复遍历,break 时生成器里的 finally 块会执行。
能不能写一个无限的迭代器?
可以,next() 永远返回 done: false 就行,比如自增 ID 生成器。用 for...of 遍历时必须自己 break;千万不要展开它或者传给 Array.from,那会一直取下去直到内存耗尽。
异步数据怎么做成可迭代的?
实现 [Symbol.asyncIterator](),它返回的迭代器的 next() 返回 Promise,用 for await...of 遍历。适合分页接口、流式读取这类数据一批批到达的场景,写成异步生成器 async *[Symbol.asyncIterator]() 最简单。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。