详解ES6可迭代协议与迭代器协议:原理、实现及for...of应用

作者:青柠时间:2026-10-22 11:49:18浏览:79

本文深入解析ES6中的可迭代协议与迭代器协议。介绍如何通过Symbol.iterator属性定义自定义迭代行为,阐述Iterator接口的统一访问机制及next方法的遍历过程。详细对比for、for...in与for...of循环的差异,并列举原生具备Iterator接口的数据结构,帮助开发者掌握高效遍历数据的方法。

ES6协议概述

ECMAScript 2015引入的几项补充并非新的内置对象或语法结构,而是定义了协议规范。这些协议允许任何遵循特定约定的对象来实现相应功能。

可迭代协议

主要涉及两个核心协议:可迭代协议(Iterable Protocol)和迭代器协议(Iterator Protocol)。

可迭代协议允许JavaScript对象定义或定制其迭代行为,例如指定在for..of结构中循环获取哪些值。部分内置类型(如Array或Map)是内置的可迭代对象,拥有默认的迭代行为;而另一些类型(如Object)则不具备这一特性。

Iterator接口的核心目的是为所有数据结构提供一种统一的访问机制,以支持for...of循环。当使用for...of遍历某数据结构时,循环会自动查找并调用Symbol.iterator方法,从而获取该对象的默认遍历器。

ES6规范规定,默认的Iterator接口应部署在数据结构的Symbol.iterator属性上。换言之,只要一个数据结构拥有Symbol.iterator属性,即可被视为“可迭代的”(iterable)。该属性本身是一个函数,即当前数据结构默认的遍历器生成函数;执行此函数将返回一个遍历器对象。

要使一个对象成为可迭代对象,该对象或其原型链上的某个对象必须包含一个名为Symbol.iterator的属性。

迭代器协议

接下来介绍迭代器协议。

该协议定义了一种生成有限或无限值序列的标准方式。

JavaScript原有的表示“集合”的数据结构主要包括Array和Object,ES6新增的有Map和Set。至此共有四种集合类型。用户还可以组合使用它们来定义自己的数据结构(例如数组成员为Map,Map成员为对象)。为此,需要一种统一的接口机制来处理所有不同的数据结构。

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

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

Iterator的遍历过程遵循以下步骤。

  1. 首先创建一个指针对象,指向当前数据结构的起始位置。遍历器对象本质上就是一个指针对象。
  2. 第一次调用指针对象的next方法,指针将指向数据结构的第一个成员。
  3. 第二次调用指针对象的next方法,指针将指向数据结构的第二个成员。
  4. 不断调用指针对象的next方法,直到指针指向数据结构的结束位置。

每次调用next方法,都会返回数据结构当前成员的信息。具体表现为返回一个包含value和done两个属性的对象。其中,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数据结构

原生具备Iterator接口的数据结构包括以下几种。

  1. Array(数组)
  2. Map
  3. Set
  4. String(字符串)
  5. TypedArray(类型化数组)
  6. 函数的arguments对象
  7. NodeList对象

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

Iterator接口调用场合

以下场合会默认调用Iterator接口(即Symbol.iterator方法)。

存在某些场景会隐式触发 Iterator 接口(即 Symbol.iterator 方法)的调用,除了下文将详述的 for...of 循环外,解构赋值与扩展运算符同样会调用默认的 Iterator 接口。

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

由于数组的遍历会调用遍历器接口,因此任何接受数组作为参数的场合,实际上都调用了遍历器接口。以下是一些常见例子。

  1. for...of循环
  2. Array.from()方法
  3. Map(), Set(), WeakMap(), WeakSet()构造函数(例如new Map([['a',1],['b',2]]))
  4. Promise.all()方法
  5. Promise.race()方法

for...of循环详解

关于for...of循环的详细说明。

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

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

在深入研究for...of循环之前,先快速回顾其他for循环,分析它们的不足之处。

传统的for循环

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

虽然for循环在遍历数组时具有优势,但某些数据结构并非数组,因此for循环并不始终适用。

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循环将只遍历对象中的值。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

作者最新文章