主题
可迭代对象
可迭代对象表示“可以被依次取值”的对象。for...of、展开运算符、Array.from() 这些语法和 API,底层都依赖可迭代协议。
先分清三个概念
最容易混淆的是这三个词:
- iterable:可迭代对象
- iterator:迭代器对象
- iteration:迭代过程
可以先记成:
- 只要对象实现了
Symbol.iterator,它就是 iterable - 调用
obj[Symbol.iterator]()得到的是 iterator - iterator 需要提供
next()
什么样的对象算可迭代
如果一个对象实现了 Symbol.iterator 方法,它就可以被 for...of 遍历:
js
const obj = {
[Symbol.iterator]() {
return {
next() {
return { done: true }
},
}
},
}为什么普通对象不能直接 for...of
普通对象默认没有实现可迭代协议:
js
const range = {
from: 1,
to: 3,
}
// for (const n of range) {} // TypeError这不是因为对象不能“遍历”,而是因为 for...of 只认可迭代协议,不认普通键值结构。
自定义一个可迭代对象
下面这个对象定义了自己的迭代规则:
js
const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let current = this.from
const end = this.to
return {
next() {
if (current <= end) {
return { value: current++, done: false }
}
return { value: undefined, done: true }
},
}
},
}
for (const n of range) {
console.log(n) // 1 2 3
}这里有两个关键点:
Symbol.iterator()返回的是迭代器对象- 迭代器对象的
next()每次返回{ value, done }
迭代器对象长什么样
js
const iterator = [1, 2][Symbol.iterator]()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: undefined, done: true }这也说明:for...of 只是帮我们自动反复调用 next()。
for...of 的底层过程
可以粗略理解成:
- 找到
obj[Symbol.iterator]() - 拿到迭代器
- 反复调用
next() - 当
done === true时结束
所以显式写出来,大概就是:
js
const str = 'Hello'
const iterator = str[Symbol.iterator]()
while (true) {
const result = iterator.next()
if (result.done) break
console.log(result.value)
}内建可迭代对象
最常见的内建可迭代对象有:
- 数组
- 字符串
MapSet
js
console.log([...new Set([1, 2, 2])]) // [1, 2]
console.log([...new Map([['a', 1]])]) // [['a', 1]]
console.log([...'abc']) // ['a', 'b', 'c']iterable 和 array-like 不是一回事
这是高频混淆点。
iterable
实现了 Symbol.iterator
array-like
只有索引和 length,看起来像数组
js
const arrayLike = {
0: 'Hello',
1: 'World',
length: 2,
}这个对象是类数组,但不是 iterable,所以不能直接 for...of:
js
// for (const item of arrayLike) {} // TypeErrorArray.from() 为什么很常出现
Array.from() 能把 iterable 或类数组都转成真正数组:
js
const str = 'abc'
console.log(Array.from(str)) // ['a', 'b', 'c']
const arrayLike = { 0: 'a', 1: 'b', length: 2 }
console.log(Array.from(arrayLike)) // ['a', 'b']这也是它比展开运算符适用范围更广的原因之一。
生成器也能直接生成 iterable
如果对象的 Symbol.iterator 是一个生成器,代码会更简洁:
js
const range = {
from: 1,
to: 3,
*[Symbol.iterator]() {
for (let value = this.from; value <= this.to; value++) {
yield value
}
},
}这也是生成器和可迭代对象经常一起出现的原因。
使用建议
- 需要让对象支持
for...of、展开运算符时,才实现可迭代协议。 - 普通键值对象不要为了“统一”而强行做成 iterable。
- 分不清 iterable 和 array-like 时,先看有没有
Symbol.iterator。 - 需要兼容两类输入并转数组时,优先想到
Array.from()。
