21. 可迭代对象和迭代器
- 21.1. 概述
- 21.1.1. 可迭代值
- 21.1.2. 支持迭代的构造函数
- 21.2. 可迭代性
- 21.3. 可迭代数据源
- 21.3.1. 数组
- 21.3.2. 字符串
- 21.3.3. Map
- 21.3.4. Set
- 21.3.5.
arguments
- 21.3.6. DOM 数据结构
- 21.3.7. 可迭代计算数据
- 21.3.8. 普通对象不可迭代
- 21.4. 迭代语言结构
- 21.4.1. 通过数组模式解构
- 21.4.2.
for-of
循环
- 21.4.3.
Array.from()
- 21.4.4. 展开运算符 (
...
)
- 21.4.5. Map 和 Set
- 21.4.6. Promise
- 21.4.7.
yield*
- 21.5. 实现可迭代对象
- 21.5.1. 可迭代的迭代器
- 21.5.2. 可选的迭代器方法:
return()
和 throw()
- 21.6. 更多可迭代对象的例子
- 21.6.1. 返回可迭代对象的工具函数
- 21.6.2. 可迭代对象的组合器
- 21.6.3. 无限可迭代对象
- 21.7. 常见问题:可迭代对象和迭代器
- 21.7.1. 迭代协议是否很慢?
- 21.7.2. 我可以多次重用同一个对象吗?
- 21.7.3. 为什么 ECMAScript 6 没有可迭代组合器?
- 21.7.4. 实现可迭代对象是否很困难?
- 21.8. 深入了解 ECMAScript 6 迭代协议
- 21.8.1. 迭代
- 21.8.2. 关闭迭代器
- 21.8.3. 检查清单
21.1 概述
ES6 引入了一种遍历数据的新机制:迭代。迭代的中心是两个概念
- 可迭代对象是一种希望将其元素公开访问的数据结构。它通过实现一个键为
Symbol.iterator
的方法来做到这一点。该方法是迭代器的工厂。
- 迭代器是一个用于遍历数据结构元素的指针(想想数据库中的游标)。
用 TypeScript 表示法表示为接口,这些角色如下所示
21.1.1 可迭代值
以下值是可迭代的
- 数组
- 字符串
- Map
- Set
- DOM 数据结构(正在进行中)
普通对象不可迭代(原因在专门章节中解释)。
21.1.2 支持迭代的构造函数
通过迭代访问数据的语言结构
- 通过数组模式解构
-
for-of
循环
-
Array.from()
:
- 展开运算符 (
...
)
- Map 和 Set 的构造函数
-
Promise.all()
, Promise.race()
-
yield*
:
21.2 可迭代性
可迭代性的概念如下。
-
数据消费者: JavaScript 具有使用数据的语言结构。例如,
for-of
循环遍历值,展开运算符 (...
) 将值插入数组或函数调用中。
-
数据源: 数据消费者可以从各种来源获取其值。例如,您可能希望迭代数组的元素、Map 中的键值对或字符串的字符。
每个消费者都支持所有来源是不切实际的,特别是因为应该可以创建新的来源(例如,通过库)。因此,ES6 引入了接口 Iterable
。数据消费者使用它,数据源实现它
鉴于 JavaScript 没有接口,Iterable
更像是一种约定
-
来源: 如果一个值有一个方法,其键是符号
Symbol.iterator
,并且该方法返回一个所谓的迭代器,则该值被认为是可迭代的。迭代器是一个对象,它通过其方法 next()
返回值。我们说:它迭代可迭代对象的项(内容),每次方法调用一个。
-
消费: 数据消费者使用迭代器来检索他们正在消费的值。
让我们看看数组 arr
的消费情况。首先,您通过键为 Symbol.iterator
的方法创建一个迭代器
然后重复调用迭代器的 next()
方法来检索数组“内部”的项
如您所见,next()
返回包装在对象中的每个项,作为属性 value
的值。布尔属性 done
指示何时到达项序列的末尾。
Iterable
和迭代器是所谓的协议(接口加上使用它们的规则)的一部分,用于迭代。此协议的一个关键特征是它是顺序的:迭代器一次返回一个值。这意味着如果可迭代数据结构是非线性的(例如树),则迭代将使其线性化。
21.3 可迭代数据源
我将使用 for-of
循环(参见“for-of
循环”一章)来迭代各种可迭代数据。
21.3.1 数组
数组(和类型化数组)在其元素上是可迭代的
21.3.2 字符串
字符串是可迭代的,但它们迭代 Unicode 代码点,每个代码点可能包含一个或两个 JavaScript 字符
21.3.3 Map
Map 在其条目上是可迭代的。每个条目都被编码为一个 [key, value] 对,一个包含两个元素的数组。条目总是以确定性的方式进行迭代,顺序与它们添加到 Map 中的顺序相同。
请注意,WeakMap 不可迭代。
21.3.4 Set
Set 在其元素上是可迭代的(迭代顺序与它们添加到 Set 中的顺序相同)。
请注意,WeakSet 不可迭代。
21.3.5 arguments
尽管特殊变量 arguments
在 ECMAScript 6 中或多或少已经过时(由于剩余参数),但它是可迭代的
21.3.6 DOM 数据结构
大多数 DOM 数据结构最终都将是可迭代的
请注意,实现此功能的工作正在进行中。但这相对容易做到,因为符号 Symbol.iterator
不会与现有的属性键冲突。
21.3.7 可迭代计算数据
并非所有可迭代内容都必须来自数据结构,它也可以动态计算。例如,所有主要的 ES6 数据结构(数组、类型化数组、Map、Set)都有三个返回可迭代对象的方法
-
entries()
返回一个对编码为 [key, value] 数组的条目的可迭代对象。对于数组,值是数组元素,键是它们的索引。对于 Set,每个键和值都是相同的——Set 元素。
-
keys()
返回一个对条目键的可迭代对象。
-
values()
返回一个对条目值的可迭代对象。
让我们看看它是什么样子的。entries()
为您提供了一种获取数组元素及其索引的好方法
21.3.8 普通对象不可迭代
普通对象(由对象字面量创建)不可迭代
为什么默认情况下对象不能在其属性上进行迭代?原因如下。您可以在 JavaScript 中进行迭代的级别有两个
- 程序级别:迭代属性意味着检查程序的结构。
- 数据级别:迭代数据结构意味着检查程序管理的数据。
将属性迭代设为默认值意味着混合这些级别,这有两个缺点
- 您不能迭代数据结构的属性。
- 一旦您迭代对象的属性,将该对象转换为数据结构将破坏您的代码。
如果引擎要通过方法 Object.prototype[Symbol.iterator]()
实现可迭代性,那么还有一个额外的注意事项:通过 Object.create(null)
创建的对象将不可迭代,因为 Object.prototype
不在它们的原型链中。
重要的是要记住,如果您将对象用作 Map1,则迭代对象的属性主要是有趣的。但我们只在 ES5 中这样做,因为我们没有更好的选择。在 ECMAScript 6 中,我们有内置数据结构 Map
。
21.3.8.1 如何迭代属性
迭代属性的正确(且安全)方法是通过工具函数。例如,通过 objectEntries()
,其实现如下所示(未来的 ECMAScript 版本可能内置了类似的功能)
21.4 迭代语言结构
以下 ES6 语言结构利用了迭代协议
- 通过数组模式解构
-
for-of
循环
Array.from()
- 展开运算符 (
...
)
- Map 和 Set 的构造函数
-
Promise.all()
, Promise.race()
yield*
以下部分详细描述了它们中的每一个。
21.4.1 通过数组模式解构
通过数组模式解构适用于任何可迭代对象
21.4.2 for-of
循环
for-of
是 ECMAScript 6 中的一个新循环。它的基本形式如下所示
有关更多信息,请参阅“for-of
循环”一章。
请注意,需要 iterable
的可迭代性,否则 for-of
无法循环遍历值。这意味着不可迭代的值必须转换为可迭代的值。例如,通过 Array.from()
。
21.4.3 Array.from()
Array.from()
将可迭代值和类数组值转换为数组。它也可用于类型化数组。
有关 Array.from()
的更多信息,请参阅关于数组的章节。
21.4.4 展开运算符 (...
)
展开运算符将可迭代对象的值插入数组中
这意味着它为您提供了一种将任何可迭代对象转换为数组的紧凑方法
展开运算符还将可迭代对象转换为函数、方法或构造函数调用的参数
21.4.5 Map 和 Set
Map 的构造函数将 [key, value] 对的可迭代对象转换为 Map
Set 的构造函数将元素的可迭代对象转换为 Set
WeakMap
和 WeakSet
的构造函数的工作方式类似。此外,Map 和 Set 本身是可迭代的(WeakMap 和 WeakSet 不是),这意味着您可以使用它们的构造函数来克隆它们。
21.4.6 Promise
Promise.all()
和 Promise.race()
接受 Promise 的可迭代对象
21.4.7 yield*
yield*
是一个只能在生成器内部使用的运算符。它会 yield 由可迭代对象迭代的所有项目。
yield*
最重要的用例是递归调用生成器(生成可迭代对象)。
21.5 实现可迭代对象
在本节中,我将详细解释如何实现可迭代对象。请注意,ES6 生成器 通常比“手动”执行此任务方便得多。
迭代协议如下所示。
如果一个对象有一个键为 Symbol.iterator
的方法(自己的或继承的),则该对象变为_可迭代的_(“实现”接口 Iterable
)。该方法必须返回一个_迭代器_,该迭代器是一个通过其方法 next()
_迭代_可迭代对象“内部”_项目_的对象。
在 TypeScript 表示法中,可迭代对象和迭代器的接口如下所示2。
return()
是一个可选方法,我们稍后会介绍3。让我们首先实现一个虚拟可迭代对象,以了解迭代的工作原理。
让我们检查一下 iterable
是否确实是可迭代的
代码执行三个步骤,计数器 step
确保所有事情都按正确的顺序发生。首先,我们返回 'hello'
值,然后返回 'world'
值,最后我们指示迭代已到达末尾。每个项目都包装在一个具有以下属性的对象中
-
value
保存实际项目,
-
done
是一个布尔标志,指示是否已到达末尾。
如果 done
为 false
,则可以省略它;如果 value
为 undefined
,则可以省略它。也就是说,switch
语句可以写成如下形式。
正如关于生成器的章节中所解释的,在某些情况下,您甚至希望最后一个带有 done: true
的项目也具有 value
。否则,next()
可以更简单,直接返回项目(不将它们包装在对象中)。然后,迭代的结束将通过特殊值(例如,符号)来指示。
让我们再来看一个可迭代对象的实现。函数 iterateOver()
返回传递给它的参数的可迭代对象
21.5.1 可迭代的迭代器
如果可迭代对象和迭代器是同一个对象,则可以简化前面的函数
即使原始可迭代对象和迭代器不是同一个对象,如果迭代器具有以下方法(这也使其成为可迭代的),则它仍然偶尔有用
所有内置 ES6 迭代器都遵循此模式(通过公共原型,请参阅关于生成器的章节)。例如,数组的默认迭代器
为什么如果迭代器也是可迭代的,这会很有用?for-of
仅适用于可迭代对象,而不适用于迭代器。因为数组迭代器是可迭代的,所以您可以在另一个循环中继续迭代
继续迭代的一个用例是,您可以在通过 for-of
处理实际内容之前删除初始项目(例如标题)。
21.5.2 可选的迭代器方法:return()
和 throw()
两个迭代器方法是可选的
-
如果迭代过早结束,
return()
会让迭代器有机会进行清理。
-
throw()
是关于将方法调用转发给通过 yield*
迭代的生成器。这在关于生成器的章节中进行了解释。
21.5.2.1 通过 return()
关闭迭代器
如前所述,可选的迭代器方法 return()
是关于让迭代器在没有迭代到末尾时进行清理。它_关闭_迭代器。在 for-of
循环中,过早(或规范语言中的_突然_)终止可能是由以下原因造成的
break
-
continue
(如果您继续外部循环,continue
的作用类似于 break
)
throw
return
在每种情况下,for-of
都会让迭代器知道循环不会完成。让我们看一个例子,函数 readLinesSync
返回文件中文本行的可迭代对象,并且希望关闭该文件,无论发生什么情况
由于 return()
,文件将在以下循环中正确关闭
return()
方法必须返回一个对象。这是由于生成器处理 return
语句的方式,将在关于生成器的章节中进行解释。
以下构造会关闭未完全“耗尽”的迭代器
for-of
yield*
- 解构
Array.from()
-
Map()
、Set()
、WeakMap()
、WeakSet()
-
Promise.all()
, Promise.race()
后面的章节提供了有关关闭迭代器的更多信息。
21.6 更多可迭代对象的例子
在本节中,我们将查看更多可迭代对象的例子。大多数这些可迭代对象更容易通过生成器实现。关于生成器的章节展示了如何实现。
21.6.1 返回可迭代对象的工具函数
返回可迭代对象的工具函数和方法与可迭代数据结构一样重要。以下是用于迭代对象自身属性的工具函数。
另一种选择是使用迭代器而不是索引来遍历具有属性键的数组
21.6.2 可迭代对象的组合器
_组合器_4 是将现有可迭代对象组合起来创建新的可迭代对象的函数。
21.6.2.1 take(n, iterable)
让我们从组合器函数 take(n, iterable)
开始,它返回 iterable
的前 n
个项目的可迭代对象。
21.6.2.2 zip(...iterables)
zip
将_n_ 个可迭代对象转换为_n_ 元组的可迭代对象(编码为长度为_n_ 的数组)。
如您所见,最短的可迭代对象决定了结果的长度
21.6.3 无限可迭代对象
有些可迭代对象可能永远不会_完成_。
对于无限可迭代对象,您不能迭代“所有”对象。例如,通过从 for-of
循环中跳出
或者只访问无限可迭代对象的开头
或者使用组合器。take()
是一种可能性
zip()
返回的可迭代对象的“长度”由其最短的输入可迭代对象决定。这意味着 zip()
和 naturalNumbers()
为您提供了对任意(有限)长度的可迭代对象进行编号的方法
21.7 常见问题解答:可迭代对象和迭代器
21.7.1 迭代协议速度慢吗?
您可能会担心迭代协议速度慢,因为每次调用 next()
都会创建一个新对象。但是,在现代引擎中,小型对象的内存管理速度很快,从长远来看,引擎可以优化迭代,从而无需分配中间对象。es-discuss 上的一个帖子提供了更多信息。
21.7.2 我可以多次重复使用同一个对象吗?
原则上,没有什么可以阻止迭代器多次重复使用同一个迭代结果对象——我希望大多数情况下都能正常工作。但是,如果客户端缓存迭代结果,就会出现问题
如果迭代器重复使用其迭代结果对象,则 iterationResults
通常将多次包含同一个对象。
21.7.3 为什么 ECMAScript 6 没有可迭代对象组合器?
您可能会想知道为什么 ECMAScript 6 没有_可迭代对象组合器_,即用于处理可迭代对象或创建可迭代对象的工具。这是因为计划分两步进行
- 步骤 1:标准化迭代协议。
- 步骤 2:等待基于该协议的库。
最终,一个这样的库或来自多个库的部分将被添加到 JavaScript 标准库中。
如果您想了解此类库的外观,请查看标准 Python 模块 itertools
。
21.7.4 实现可迭代对象不难吗?
是的,可迭代对象很难实现——如果您手动实现它们的话。下一章将介绍_生成器_,它们可以帮助完成这项任务(以及其他任务)。
21.8 深入了解 ECMAScript 6 迭代协议
迭代协议包含以下接口(我从 Iterator
中省略了 throw()
,它仅由 yield*
支持,并且是可选的)
21.8.1 迭代
next()
的规则
- 只要迭代器还有要生成的值
x
,next()
就会返回对象 { value: x, done: false }
。
- 迭代完最后一个值后,
next()
应始终返回一个属性 done
为 true
的对象。
21.8.1.1 IteratorResult
迭代器结果的属性 done
不必是 true
或 false
,真值或假值就足够了。所有内置语言机制都允许您省略 done: false
。
21.8.1.2 返回新迭代器的可迭代对象与始终返回相同迭代器的可迭代对象
某些可迭代对象每次被请求时都会生成一个新的迭代器。例如,数组
其他可迭代对象每次都返回相同的迭代器。例如,生成器对象
可迭代对象是生成新的迭代器还是不生成新的迭代器,这取决于您是否多次迭代同一个可迭代对象。例如,通过以下函数
使用新的迭代器,您可以多次迭代同一个可迭代对象
如果每次都返回相同的迭代器,则不能
请注意,标准库中的每个迭代器也是一个可迭代对象。它的方法 [Symbol.iterator]()
返回 this
,这意味着它始终返回相同的迭代器(自身)。
21.8.2 关闭迭代器
迭代协议区分两种完成迭代器的方式
- 耗尽:完成迭代器的常规方法是检索其所有值。也就是说,调用
next()
直到它返回一个属性 done
为 true
的对象。
- 关闭:通过调用
return()
,您告诉迭代器您不打算再调用 next()
了。
调用 return()
的规则
-
return()
是一个可选方法,并非所有迭代器都有。具有该方法的迭代器称为_可关闭的_。
-
只有当迭代器尚未耗尽时,才应调用
return()
。例如,每当“突然”(在完成之前)离开时,for-of
都会调用 return()
。以下操作会导致突然退出:break
、continue
(带有外部块的标签)、return
、throw
。
实现 return()
的规则
- 方法调用
return(x)
通常应生成对象 { done: true, value: x }
,但如果结果不是对象,则语言机制只会抛出错误(规范中的来源)。
- 调用
return()
后,next()
返回的对象也应该是 done
。
以下代码说明了如果在收到 done
迭代器结果之前中止 for-of
循环,则 for-of
循环会调用 return()
。也就是说,即使在收到最后一个值后中止,也会调用 return()
。这很微妙,在手动迭代或实现迭代器时必须小心处理才能正确。
21.8.2.1 可关闭迭代器
如果迭代器具有 return()
方法,则它是*可关闭的*。并非所有迭代器都是可关闭的。例如,数组迭代器不是
生成器对象默认是可关闭的。例如,以下生成器函数返回的对象
如果在 elements()
的结果上调用 return()
,则迭代完成
如果迭代器不可关闭,则可以在从 for-of
循环突然退出(例如行 A 中的退出)后继续对其进行迭代
相反,elements()
返回一个可关闭的迭代器,并且 twoLoops()
内部的第二个循环没有任何可迭代的内容
21.8.2.2 防止迭代器被关闭
以下类是防止迭代器被关闭的通用解决方案。它通过包装迭代器并转发除 return()
之外的所有方法调用来实现。
如果我们使用 PreventReturn
,则在 twoLoops()
的第一个循环突然退出后,生成器 elements()
的结果不会被关闭。
还有另一种使生成器不可关闭的方法:生成器函数 elements()
生成的所有生成器对象都具有原型对象 elements.prototype
。通过 elements.prototype
,您可以隐藏 return()
的默认实现(位于 elements.prototype
的原型中),如下所示
21.8.2.3 通过 try-finally
处理生成器中的清理
一些生成器需要在对其进行迭代完成后进行清理(释放分配的资源、关闭打开的文件等)。简单地说,这就是我们实现它的方式
在普通的 for-of
循环中,一切正常
但是,如果在第一个 yield
之后退出循环,则执行似乎会永远暂停在那里,并且永远不会到达清理步骤
实际发生的情况是,每当提前离开 for-of
循环时,for-of
都会向当前迭代器发送 return()
。这意味着无法到达清理步骤,因为生成器函数会事先返回。
幸运的是,这很容易解决,方法是在 finally
子句中执行清理
现在一切按预期工作
因此,使用需要以某种方式关闭或清理的资源的通用模式是
21.8.2.4 处理手动实现的迭代器中的清理
请注意,当您要第一次返回 done
迭代器结果时,必须调用 cleanUp()
。您不得过早地执行此操作,因为那时可能仍然会调用 return()
。这可能很难做到正确。
21.8.2.5 关闭您使用的迭代器
如果您使用迭代器,则应正确关闭它们。在生成器中,您可以让 for-of
为您完成所有工作
如果您手动管理,则需要更多工作
如果您不使用生成器,则需要做更多的工作
21.8.3 检查表
- 记录可迭代对象:提供以下信息。
- 它是每次都返回新的迭代器还是相同的迭代器?
- 它的迭代器可关闭吗?
- 实现迭代器
- 如果迭代器已耗尽或调用了
return()
,则必须执行清理活动。
- 在生成器中,
try-finally
允许您在一个位置处理两者。
- 通过
return()
关闭迭代器后,它不应再通过 next()
生成任何迭代器结果。
- 手动使用迭代器(相对于通过
for-of
等)
- 不要忘记通过
return
关闭迭代器,前提是(且仅当)您没有耗尽它。正确处理这一点可能很棘手。
- 在突然退出后继续迭代迭代器:迭代器必须是不可关闭的,或者被设置为不可关闭的(例如,通过工具类)。