ES6系列--7. 可迭代协议和迭代器协议

824 阅读6分钟

ECMAScript 2015的几个补充,并不是新的内置或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现。 有两个协议:可迭代协议和迭代器协议。

可迭代协议

可迭代协议允许 JavaScript 对象去定义或定制它们的迭代行为, 例如(定义)在一个 for..of 结构中什么值可以被循环(得到)。一些内置类型都是内置的可迭代对象并且有默认的迭代行为, 比如 Array or Map, 另一些类型则不是 (比如Object) 。

Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环(详见下文)。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口,调用Symbol.iterator方法,返回该对象的默认遍历器。

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可迭代的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。

为了变成可迭代对象, 一个对象必须实现(或者它原型链的某个对象)必须有一个名字是 Symbol.iterator 的属性:

迭代器协议

该迭代器协议定义了一种标准的方式来产生一个有限或无限序列的值。

JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了MapSet。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是MapMap的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

迭代器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

Iterator 的遍历过程是这样的。

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

  3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

  4. 不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

var someString = "hi";
typeof someString[Symbol.iterator]; // "function"

var iterator = someString[Symbol.iterator]();
iterator + "";    // "[object String Iterator]"

iterator.next()       // { value: "h", done: false }
iterator.next();      // { value: "i", done: false }
iterator.next();      // { value: undefined, done: true }

原生具备 Iterator 接口的数据结构如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

注意对象是不具备 Iterator 接口的,一个对象如果要具备可被for...of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。

调用 Iterator 接口的场合

有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法),除了下文会介绍的for...of循环,解构赋值, 扩展运算符其实也会调用默认的Iterator接口。

实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。

由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。

  • for...of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
  • Promise.all()
  • Promise.race()

for...of

for...of 循环是最新添加到 JavaScript 循环系列中的循环。

它结合了其兄弟循环形式 for 循环和 for...in 循环的优势,可以循环任何可迭代(也就是遵守可迭代协议)类型的数据。默认情况下,包含以下数据类型:String、Array、MapSet,注意不包含 Object 数据类型(即 {})。默认情况下,对象不可迭代。

在研究 for...of 循环之前,先快速了解下其他 for 循环,看看它们有哪些不足之处。

for 循环

for 循环的最大缺点是需要跟踪计数器和退出条件。我们使用变量 i 作为计数器来跟踪循环并访问数组中的值。我们还使用 Array.length 来判断循环的退出条件。

虽然 for 循环在循环数组时的确具有优势,但是某些数据结构不是数组,因此并非始终适合使用 loop 循环。

for...in 循环

for...in 循环改善了 for 循环的不足之处,它消除了计数器逻辑和退出条件。但是依然需要使用 index 来访问数组的值.

此外,当你需要向数组中添加额外的方法(或另一个对象)时,for...in 循环会带来很大的麻烦。因为 for...in 循环循环访问所有可枚举的属性,意味着如果向数组的原型中添加任何其他属性,这些属性也会出现在循环中。这就是为何在循环访问数组时,不建议使用 for...in 循环。

注意: forEach 循环 是另一种形式的 JavaScript 循环。但是,forEach() 实际上是数组方法,因此只能用在数组中。也无法停止或退出 forEach 循环。如果希望你的循环中出现这种行为,则需要使用基本的 for 循环。

for...of 循环

for...of 循环用于循环访问任何可迭代的数据类型。

const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

for (const digit of digits) {
  console.log(digit);
}

可以随时停止或退出 for...of 循环。

const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

for (const digit of digits) {
  if (digit % 2 === 0) {
    continue;
  }
  console.log(digit); //1,3,5,7,9
}

不用担心向对象中添加新的属性。for...of 循环将只循环访问对象中的值。