先记住这个答案
区别在于谁记录遍历进度。数组这类可迭代对象只负责「造迭代器」,每次 for...of 都会调用一次 [Symbol.iterator]() 拿到一个新迭代器,进度记在新迭代器里,所以可以反复遍历。生成器对象本身就是迭代器,同时它的 [Symbol.iterator]() 返回的是它自己,所以它也能被 for...of 遍历。但不管遍历几次,用的都是同一个进度,第一次遍历完,第二次就什么都拿不到了。
- 可迭代对象有
[Symbol.iterator](),负责造迭代器 - 迭代器有
next(),负责记住遍历到哪了 - 生成器对象两者都是,
[Symbol.iterator]()返回它自己 - 生成器对象只能遍历一次,想重来就重新调用生成器函数
同样遍历两次,结果不一样
这是面试里最能说明问题的对比。
const arr = [1, 2, 3]
console.log([...arr].join(','), [...arr].join(',')) // 两次都完整
function* numbers() {
yield 1
yield 2
yield 3
}
const gen = numbers()
console.log(gen[Symbol.iterator]() === gen) // 返回的是它自己
console.log(gen.next().value) // 先手动取走一个
console.log([...gen].join(',')) // 只剩后面两个
console.log([...gen].length) // 已经空了
console.log([...numbers()].join(',')) // 重新调用才有新的查看输出与解释
1,2,3 1,2,3
true
1
2,3
0
1,2,3数组展开两次都完整,因为每次展开都新建了一个数组迭代器。生成器对象先被 next() 取走了 1,展开时只剩 2 和 3,再展开一次就是空的。想从头开始,只能重新调用 numbers() 得到一个新的生成器对象。
两个协议分别要求什么
JavaScript 的遍历靠两个约定。
- 可迭代协议
- 对象上有一个
[Symbol.iterator]方法,调用后返回一个迭代器。for...of、展开运算符、Array.from、解构赋值、Promise.all都是先调用这个方法。 - 迭代器协议
- 对象上有一个
next()方法,每次调用返回{ value, done }。done为true表示结束。另外还可以有可选的return()方法,for...of中途break或抛错时会调用它,让迭代器做清理。 生成器对象把两个协议都实现了,而且return()和throw()都有。所以它能当迭代器手动调用next(),也能直接丢给for...of。
容易答错的地方
- 把可迭代对象和迭代器当成一个东西
- 可迭代对象是「能造出迭代器的东西」,迭代器是「正在遍历的那个游标」。数组是可迭代对象但不是迭代器,数组上没有
next()方法。生成器对象恰好同时是两者。 - 把生成器对象缓存起来给多处使用
- 比如把
const list = numbers()传给两个组件,一个组件遍历完,另一个就拿不到数据了,两处还会互相抢进度。需要多次遍历时,缓存生成器函数本身,或者先[...gen]转成数组。
面试官还会怎么问?
怎么写一个可以反复遍历的对象?
让 [Symbol.iterator] 每次都返回一个新的迭代器。最省事的写法是把它写成生成器方法:const range = { *[Symbol.iterator]() { for (let i = 0; i < 3; i++) yield i } }。每次 for...of 都会重新调用这个方法,得到一个新的生成器对象,所以能遍历任意多次。
普通对象为什么不能 for...of?
普通对象没有 [Symbol.iterator] 方法,不满足可迭代协议,for...of 会报 TypeError: obj is not iterable。可以用 Object.entries(obj) 转成数组再遍历,或者用 for...in 遍历键名。
数组的迭代器是什么?
arr[Symbol.iterator]() 返回的是一个 Array Iterator 对象,它和生成器对象一样,[Symbol.iterator]() 也返回自己,同样只能遍历一次。arr.values()、arr.entries() 返回的也是这种迭代器。区别只在于数组本身每次能造出新的。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。