js 中的迭代器与可迭代对象

540 阅读5分钟

在平常项目中,调试时在数组等可迭代对象上看到过下图显示的 Symbol.iterator 属性:

image.png

它是 js 内置的一个 Symbol 值,指向该对象的默认的迭代器方法。某个对象是否能用 for of 遍历,判断的依据就是该对象有没有部署 Symbol.iterator 接口。下面就来说说,到底什么是迭代器,什么又是可迭代对象。

迭代器

在 js 中,迭代器是一个实现了迭代器协议( Iterator protocol )的对象。通过迭代器这一接口机制,让用户可以对各种不同的容器对象(数组、链表SetMap 等)进行遍访。

迭代器协议

迭代器协议定义了产生一系列值的标准方式,在 js 中就是指一个特定的 next() 方法 —— 一个无参数或只接收 1 个参数的函数。next() 方法一般是不接收参数的,但后续文章会介绍的生成器,作为一种特殊的迭代器,它的 next() 方法就可以接收 1 个参数。next() 的返回值是一个拥有 2 个属性的对象,valuedone

  • value

迭代器返回的任何 JavaScript 值。donetrue 时可省略,如果此时 value 依然存在,即为迭代结束之后的默认返回值。

  • done

值为布尔类型,如果迭代器可以产生序列中的下一个值,则为 false。如果迭代器已将序列迭代完毕,则为 true

迭代器的实现

现在我们就根据迭代器协议来实现一个迭代器,它无非就是一个拥有特定 next() 方法的对象而已:

// 例 1
const arr = [1, 2, 3]
let index = 0
const iterator = {
  next() {
    if (index < arr.length) {
      return { value: arr[index++], done: false }
    } else {
      return { value: undefined, done: true }
    }
  }
}

例 1 的 iterator 就是一个迭代器,它可以帮助我们遍历数组 arr

// 例 1.1
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true }

可迭代对象

可迭代对象是实现了可迭代协议(Iterable protocol,注意这里是 Iterable,而迭代器协议是 Iterator )的对象。可迭代协议要求,可迭代对象必须实现一个叫做 @@iterator 的方法,我们可以使用 js 内建的常量 Symbol.iterator 来访问该方法,而这个方法的返回值,是一个迭代器。下面我们实现一个可迭代对象 iterableObj:

// 例 2
const iterableObj = {
  arr: [1, 2, 3],
  [Symbol.iterator]: function () {
    let index = 0
    return {
      next: () => {
        if (index < this.arr.length) {
          return { value: this.arr[index++], done: false }
        } else {
          return { value: undefined, done: true }
        }
      }
    }
  }
}

这里需要注意下 next 方法中 this 的指向问题,第 8 行我们之所以获取得到 arr 的 length,是因为我们用了箭头函数定义的 next 方法,那么 this 指向的就是上层作用域,也就是 [Symbol.iterator] 指向的函数的作用域,而该函数我们是用 function 定义的,所以当其被 iterableObj 调用时,其中的 this 指向的就是 iterableObj,所以我们在 next 方法中可以通过 this 去获取 iterableObj 的 arr。

现在,每次对 iterableObj[Symbol.iterator] 进行调用,得到的都是一个新的迭代器,可以通过迭代器的 next() 方法进行迭代:

// 例 2.1
const iterator1 = iterableObj[Symbol.iterator]()
console.log(iterator1.next()) // { value: 1, done: false }
console.log(iterator1.next()) // { value: 2, done: false }
console.log(iterator1.next()) // { value: 3, done: false }
console.log(iterator1.next()) // { value: undefined, done: true }

const iterator2 = iterableObj[Symbol.iterator]()
console.log(iterator2.next()) // { value: 1, done: false }

可迭代对象的应用

for of

我们知道普通的对象是不能用 for of 进行遍历的,因为一般的对象都不是可迭代对象,也就是它本身(或者它原型链上的某个对象)没有一个键为 @@iterator 的属性。而例 2 中我们自己的定义的 iterableObj,是一个可迭代对象,可以用 for of 遍历:

// 例 2.2
for (const iterator of iterableObj) {
  console.log(iterator)
}

打印结果如下:

image.png

for of 本质上可以看成是一种语法糖,其背后是执行了像例 2.1 这样的代码,把执行 @@iterator 方法得到的迭代器每次调用 next() 得到的对象的 value 给返回出来。如果我们将例 2 第 8 行的 if 判断条件改为 if (index < this.arr.length - 1),那么例 2.2 的 for of 就会少遍历得到一个 3

js 中,Array,Map,Set,String,TypedArray,arguments,NodeList 对象等已经实现了可迭代协议,所以它们都可以使用 for of 进行遍历。我们自己也可以创建一个类,让这个类的实例是可迭代对象,其实实现的原理也是给这个类一个 [Symbol.iterator] 实例方法,让该方法返回一个迭代器:

// 例 2.2.3
class IterableClass {
  constructor(arr) {
    this.arr = arr
  }
  [Symbol.iterator]() {
    let index = 0
    return {
      next: () => {
        if (index < this.arr.length) {
          return { value: this.arr[index++], done: false }
        } else {
          return { value: undefined, done: true }
        }
      },
      return: () => {
        console.log('监听到迭代提前终止')
        return { done: true }
      }
    }
  }
}

const iterable = new IterableClass([1, 2, 3])
for (const iterator of iterable) {
  console.log(iterator)
  if (iterator === 2) break
}

监听迭代提前终止

在例 2.2.3 的第 16 行我们还给迭代器写了个 return() 方法,用于监听像第 27 行发生的提前终止迭代的行为。return() 方法也需要返回一个拥有 value 和 done 属性的对象,由于 done 为 true,所以 value 可以省略。执行结果如下图:

image.png

展开语法

在调用函数或构造数组时使用的展开语法本质上也是用到了迭代器,所以只适用于可迭代对象。而 ES2018(ES9) 添加的在构造字面量对象时使用的展开语法,本质上并不是使用了迭代器,所以对象不能用 for of 但是可以使用展开语法。

解构数组

解构数组实际上也是通过迭代器,把数组里的值一个个通过 value 去获取:

// 例 3
const arr = [1, 2, 3]
const [a, b, c] = arr
console.log(a, b, c) // 1 2 3

但是 ES2018(ES9) 新添加的解构对象则不是。

接受可迭代对象作为参数的内置 API

诸如 new Map([iterable])Array.from(iterable)Promise.all(iterable) 等,可参见 MDN 文档。

感谢.gif 点赞.png