Skip to content

可迭代对象

可迭代对象表示“可以被依次取值”的对象。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 的底层过程

可以粗略理解成:

  1. 找到 obj[Symbol.iterator]()
  2. 拿到迭代器
  3. 反复调用 next()
  4. 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)
}

内建可迭代对象

最常见的内建可迭代对象有:

  • 数组
  • 字符串
  • Map
  • Set
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) {} // TypeError

Array.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()

基于 MIT 许可发布