JavaScript迭代器和生成器:同步迭代器

英文原文: https://dev.to/jfet97/javascript-iterators-and-generators-synchronous-iterators-141d

介绍

要指出的第一件事是,迭代在JavaScript是基于一个协议:一组取代会一直约定的接口与接口的支持的语言。
无论如何,ECMAScript规范主要使用“接口”这个词,因此我也会这样做

对于这个概念可能是新概念,您应该将接口想象为代码中两个实体之间的契约。如果没有合同,这两个部分会相互了解太多,彼此过于依赖:内部的变化可能会迫使我们改变另一个。

回到迭代器,它们的主要目的是允许使用给定集合的每个元素。它们非常好,消费者不需要知道集合如何存储和管理这些元素。
只要迭代器接口得到尊重,消费者和集合就会完全独立:我们将能够在不影响消费者的情况下更改集合的内部细节。事实上,我们可以用另一个集合替换整个集合。

与此同时,该系列对消费者一无所知。因此,消费者可以迭代它们也彼此非常不同。

Iterable,Iterator和IteratorResult接口

整个迭代事物基于三个接口。

Iterable

Iterable接口定义了一个实体来实现要考虑的一个迭代。

规范说,iterable有一个必需的方法,即@@iterator方法,它返回一个实现Iterator接口的对象。

什么是@@iterator?是我们可以在Symbol构造函数中找到的特定符号:Symbol.iterator

Iterator

Iterator接口定义了一个实体来实现被认为是一个迭代器。

规范说迭代器有一个必需的方法:next方法。此方法是迭代本身的支点,它返回一个实现IteratorResult接口的对象。它的主要目的是从集合中获取后续元素,直到没有更多条目。

还有另外两种可选方法:return方法和throw方法。如果它们返回一个值,则它必须是IteratorResult

前者的目的是向迭代器发出信号,即消息不再调用下一个方法,即使迭代尚未到达终点。通过这种方式,迭代器将能够执行它可能需要执行的任何清理。

后者的目的是向迭代器发出信号,表明消费者遇到了错误。

这三种方法都至少接受一个参数:

  • next方法应该能够接收一个或多个参数,但作为规范说“他们的解释和有效性取决于目标迭代”。
  • return方法应该返回回所接收到的参数,它插入到返回IteratorResult
  • throw方法应该抛出所接收的参数,它通常是一个例外。

记得在调用之前检查最后两种方法的存在!

IteratorResult

IteratorResult接口定义了一个实体来实现被认为是一个通用的迭代步骤的一个有效的结果。

规范说迭代器结果有两个字段,两个字段都是可选的:done字段和value字段。

done的字段是信号的迭代的端部的布尔标志。只要迭代器返回的值有效,它就应该为false,然后在返回最后一个有效值后变为true。如果省略done标志,则认为其值为false

value字段可以包含任何有效的ECMAScript值。它是每个迭代步骤的当前迭代值,直到done标志变为true。之后,这是迭代器的返回值,如果它提供了一个。如果省略value标志,则认为其值为undefined

约定

创建格式良好的迭代器时应遵循一些通用约定:

  1. 每次调用@@iterator方法时,每个Iterable实体都应该返回一个新的迭代器。
  2. 每个Iterator实体也应该是一个Iterable(稍后会详细介绍),使用一个只返回它的函数实现@@iterator方法,这是对迭代器本身的引用。这是先前规则的受控例外。
  3. 报告明确每个迭代步骤的有效valuedone设置为标志false。不要将相关值与done:true
  4. 如果调用了return方法或throw方法,那么迭代器应该被认为是耗尽的,并且next方法的任何后续调用都不应该返回任何有效值。无论如何,如果当前的return/throw调用返回一个值,它应该与done: true
  5. 返回最后一个value后,应返回next方法{ value: undefined, done: true }。不要抛出错误。只需在指示的情况下发出迭代结束信号,即使在此之后多次调用next方法。

默认迭代器

让我们看看如何使用该语言中已经存在的最常见的迭代器之一。因为数组默认实现@@ iterator方法,所以获取迭代器来迭代数组是非常简单的:

现在让我们使用它:

您可以看到我们之前谈到的一些约定如何得到尊重:

  • 最后的有效value(3)耦合于一个done:false,而不是一个done:true
  • 我故意走出界限向你展示没有错误被抛出。相反,我们总是回来{ value: undefined, done: true }。

数组不是唯一的内置迭代:String对象,TypedArrays,Maps和Sets也是可迭代的,因为它们的每个原型对象都实现了@@iterator方法。

使用迭代器

让我们创建一个带有可迭代和回调的函数,将每个迭代器结果的值传递给后者:

按照其使用示例:

for-of循环

幸运的是,我们不需要编写这样的函数:JavaScript为我们提供了一种专用语法:for-of循环。

其逻辑可归纳为以下几点:
1. 调用iterable的@@iterator方法。
2. 不带参数调用接收迭代器的next方法。
3. 检查done刚收到的迭代器结果的标志是否设置为true;如果是这样,请跳到第5点,否则,继续。
4. 向客户端提供迭代器结果中包含的值,然后跳转到第2点。
5. 如果方法存在,则丢弃迭代器结果并调用迭代器的return方法,不带参数。

数组解构和数组扩展运算符

在指令之后const [a, b] = iterable;并且const clone = […iterable]存在相同的机制:迭代器是从迭代中获取的,通常仅部分地与数组解构一起使用,但直到它与数组扩展运算符一起使用

自定义迭代器

让我们最后看到上面讨论的三个接口的两个自定义实现:集合的迭代器和生产者的迭代器。

采集

以下简单集合存储已加入特定电报组的用户的所有名称。布尔标志指示用户是否是该组的管理员:

现在,让我们创建一个只返回管理员名称的迭代器。

首先要做的是使users集合成为可迭代的,并为其添加@@iterator方法:

如果您考虑一下,我们刚刚满足了从ECMAScript角度正确实现迭代器接口所需的所有要求。

显然,结果远非我们真正需要的。我们甚至不尊重上述惯例。

但是,只是为了让你知道,users现在是一个完整的可迭代的,不仅可以安全地使用for-of循环,还可以安全地使用数组解析和数组扩展运算符。

现在足够有趣,让我们添加我们需要的逻辑。

要做的第二件事是使用对象的键获取一个数组,我们将使用索引进行迭代。对于每个生成的迭代器,键数组和索引都必须是唯一的,因此它们将在调用@@iterator方法期间创建:

在每个迭代步骤中,我们必须在keys数组内部前进一个或多个位置,跳过那些与集合true内部的布尔标志不匹配的位置users。

值得注意的是,我已经更改了next方法签名,将其转换为箭头函数以保留this上下文:

不要害怕!this[keys[index]]。

请记住,这keys是一个包含users对象键的数组,因此,包含telegram组用户名的数组。因此,keys[index]将在给定索引处返回密钥,即用户的名称。之后,this[key]将根据用户状态返回truefalse

现在让我们关注我们{ done: true }为方便起见而设置的迭代器结果。

如果你问,我们可能已经返回了一个空对象,{}并且无论如何都会尊重IteratorResult接口。但是,不要忘记缺少完成标志意味着done:false。因此,例如,如果您曾尝试users在for-of循环中使用该对象,那么您可能会卡住浏览器,因为它for-of永远不会结束。

为了正确设置done标志,我们可以检查是否由于数组的length属性而超出了键的数量keys。

要设置每个返回value,因为我们需要管理员的名称,我们只需获取与当前索引对应的名称:

一个必要的题外话

我们来一个数组:

并for-of用它提供一个循环:

我们已经知道了输出:

那怎么样:

你能指望什么?例外?错误!
结果将完全相同:

让我们尝试使用我们的自定义迭代:

你能指望什么?和以前一样的输出(我的意思是andrew, clare)?错误!
你会得到一个例外:TypeError: userIterator is not iterable。

当然,userIterator不是可迭代的,它是迭代器!但是等一下,为什么它不会对array迭代器大惊小怪呢?有什么不同?
事实是,默认迭代器就像迭代器一样array,都是迭代器和迭代器。

生产商

迭代者发光的另一个地方是生产商的土地。
生产者是有状态函数,其返回值取决于先前返回的值。一个典型的例子是一个函数,它返回从0开始的下一个Fibonacci系列。让我们尝试编码:

如果你打电话,会发生这种情况fibonacciProducer:

除了缺少整数owerflow检查之外,这个实现没有任何问题,但是我们可以改进它添加迭代器,以便它可以与所有可以与迭代一起使用的ES6功能一起使用,比如for-of循环。
我们开始做吧:

以下是如何使用增强功能fibonacciProducer:

由于迭代器接口,我们还可以重新开始生成开箱即用的价值:请求另一个迭代器就足够了!

IterableIterator模式

不幸的是,以下代码继续抛出异常:

因为fibonacciIterator它是iterator而不是iterable

注意这个事实:没有一个集合可以直接给出for-of循环,就像我们可以用前一个数组做的那样。

我们只能从中获取迭代器fibonacciProducer。

这是一个预期但也是一个有用的边界,可以帮助您理解IterableIterator模式。

事实上,我们必须处理与任何集合断开连接的迭代器,如生产者案例,或者从我们无法直接到达的集合派生。

我们可以从集合中创建一个迭代器,并且我们只为函数提供了迭代器:子例程将无法直接使用集合。另一个例子:集合是类的私有成员,因此无法直接访问。但是,该类提供了一个返回该集合的迭代器的方法。

如果我们希望能够使用为这些类型的迭代器的迭代编写的ES6特性,我们必须强制它们也是迭代的。

  • 怎么样?如果像迭代一样使用,迭代器应该返回自己:

  • 这是什么意思?记住像for-of 循环一样的实用程序:它们调用@@iterator方法从迭代中获取iterable。如果请求iterator的实体本身就是一个iterator,它应该只返回自己!

不要将这一点视为可以忘记的特殊情况。默认情况下,语言中存在的
All the iterables提供了使用此特性定义的迭代器。您可以使用任何语言功能获得的All the iterators(如生成器)都遵循此约定。
因此,总是实现也是迭代的迭代器是一个好主意,即使因为您不太可能事先知道如何使用您创建的集合和迭代器。

结论

我们在关于IteratorsGenerators的第一篇文章的最后。

我们已经了解了为什么iterator模式如此重要,JavaScript如何希望我们实现三个接口(Iterable,Iterator,IteratorResult)和一些我们应该遵守的有用约定。

然后我们看到了该语言提供的默认迭代器的示例,如何将其与最常见的ES6功能一起使用,以及应该如何创建可以使用哪些功能的迭代器。

我们还详细分析了如何为自定义集合和生产者定义自定义迭代。

英文原文:https://dev.to/jfet97/javascript-iterators-and-generators-synchronous-iterators-141d

李, 国轩