使用 Symbol.Iterator 理解 JavaScript/TypeScript 中的迭代器模式
在我撰写的“设计模式”系列文章中,我以通俗易懂的方式阐述了四人组(GoF)的23种设计模式。最近,我介绍了迭代器模式。这种模式具有将数据结构与算法解耦的巨大潜力。
这种模式在不同的编程语言中得到了广泛的实现,例如,JAVA 有Iterable 接口。
在某些语言中,存在一些高级控制结构,允许你无需创建迭代器模式即可迭代对象(因为它们默认提供了迭代器模式)。然而,修改这种内部模式可能很有用,以便充分利用语言的潜力,而无需编写冗长的代码,例如在 Java 中。
了解迭代器模式并阅读我写的文章是至关重要的,因为本文中的示例都来自前一篇文章。
ES6 引入了一种与 JavaScript 数据结构交互的新方式——迭代。迭代器模式有两个核心概念:
-
Iterable是一种数据结构,它提供了一种将其数据公开的方式。在 JavaScript 中,它的实现基于一个名为Symbol.iterator的方法。实际上,Symbol.iterator 是一个迭代器工厂。
-
迭代器是一个结构体,其中包含指向迭代中下一个元素的指针。
因此,可迭代对象必须是具有函数迭代器的对象,其键为 Symbol.iterator。
此外,迭代器必须是一个包含名为 next 的函数的对象,该函数返回一个包含以下键的对象:
-
值:迭代中的当前项
-
done:如果迭代已完成则为 true,否则为 false。
因此,前面的代码定义了一个可迭代对象和一个迭代器,它们的使用方式如下:
使用 for-of 循环时,编程语言会使用一个可迭代对象,并创建它的迭代器。它会不断调用 next() 方法,直到 done 为真为止。
JavaScript 中有很多可迭代对象,它们原生使用了前面提到的概念。
-
对数组中的每个元素进行数组和类型化数组操作。
-
每个字符上方都添加了字符串。
-
集合在其元素之上。
-
映射其键值对。
JavaScript 中其他一些使用可迭代对象的结构如下:
接下来,我将向您展示如何使用 JavaScript/TypeScript Symbols 实现这种模式。在本例中,我创建了一个问题:有一个名为 WordsCollection 的类,它定义了一个单词列表(items)及其获取和添加元素的方法(getItems 和 addItem)。客户端使用诸如 for 或 forEach 之类的控制结构来调用这个类。下面的 UML 图展示了我刚才描述的场景,并使用了经典的迭代器模式。
现在我将向你们展示新的 UML 图,它保持了与自定义迭代器的兼容性,并实现了原生迭代器。
首先,我们来看客户端,它将直接使用迭代器,无需实例化任何类。它将直接从 for-of 循环中执行迭代。
请注意,原生使用迭代器会使语言更加简洁,因为它与编程语言的控制结构集成在一起。
关联的 WordsCollection 代码如下:
首先需要注意的是,我们需要实现两个接口:
-
聚合器用于自定义可迭代对象,并定义对象可迭代所需的必要方法。这里我们需要实现迭代器的构造函数方法。
-
迭代器是该语言原生使用的,它定义了函数 [Symbol.iterator]: ()。
原生迭代器的关键在于原生迭代器的工厂函数,这些函数没有直接实现,而是被提取到一个新类中,以保持代码与之前的解决方案(NativeAlphabeticalOrderIterator)一样简洁。
最后,原生迭代器和自定义迭代器的 `next()` 方法都继承自一个抽象迭代器,该抽象迭代器实现了其余方法。两者 `next()` 方法的主要区别在于它们返回的值:原生迭代器必须满足 `Iterator` 接口,并返回一个 `IteratorResult` 对象;而自定义迭代器则直接返回值。
这段代码甚至可以重构,以便与其他设计模式(例如模板方法或策略模式)一起使用。但是,我认为最好不要通过添加这些模式来使示例更加复杂。
客户端与 WordsCollection 类的内部结构解耦(单一职责),并且您可以通过实现新的迭代器来扩展软件(开放/封闭)。
我创建了两个 npm 脚本,它们在应用迭代器模式后运行此处显示的代码示例。
npm run example1-problem
npm run example1-iterator-solution-1
以下示例在后续文章中有详细描述,因此我建议您阅读该文章以更好地理解。不过,为了让您对我们正在开发的内容有所了解,我将简要描述一下问题。
假设我们需要开发一款软件,用于向社交网络中的联系人发送电子邮件,并且需要区分邮件类型。我们的联系人网络分为两类:朋友和同事。根据收件人的类型,邮件的正式程度也会有所不同。
首先,我们从两个知名的社交网络 Dev.to 和 Medium 导入联系人。这两个社交网络的数据结构实现方式不同,Dev.to 使用数组来维护联系人信息,而 Medium 使用 Map。
下面有一个 gif 动画,展示了客户端如何使用我们的整个结构(我已经做了一个简单的 CLI 示例)。
在下面的 UML 图中,您可以看到使用 CustomIterator 解决此问题的方案:
包含原生迭代器的解决方案对应的示意图如下所示。无论如何,我们将继续保持与自定义迭代器的兼容性。
开发解耦软件的主要优势在于,随着软件的增长,它不会受到变更的影响。事实上,由于应用程序的客户端是基于接口并使用依赖注入构建的,因此它仍然是同一段代码。
sendSpamToFriends 和 sendSpamToCoworkers 方法使用迭代器,可以是自定义迭代器,也可以是原生迭代器。
这种方法同时使用了自定义迭代器和原生迭代器。原生迭代器的创建被委托给迭代器工厂,以便区分好友迭代器和同事迭代器。
社交网络(dev.to 或 medium)必须通过创建 Iterable 类型的对象来满足 SocialNetwork 接口的原生迭代器要求。与社交网络相关的代码如下:
每个社交网络(dev.to 或 medium)都必须通过 Iterable 类型的对象创建原生迭代器来满足 SocialNetwork 接口。
以下是与迭代器相关的代码。自定义迭代器和原生迭代器都继承自一个父迭代器,父迭代器中已实现了它们共有的方法。虽然在我们的示例中,Iterator 接口包含多个方法,但我们只需要实现 `next()` 方法。
与 medium 关联的迭代器与 dev.to 的迭代器具有相同的接口,如下所示:
我创建了一个 npm 脚本,它在应用迭代器模式和 CLI 界面后运行此处显示的示例。
npm run example2-iterator-solution1
迭代器模式可以避免项目中的代码耦合。当集合中包含多个算法和数据结构时,迭代器模式非常适用。由于应用了单一职责和开闭原则这两个著名的原则,你的代码会更加简洁。
有时我们想了解我们正在开发的编程语言,并检查迭代器模式是否像 JavaScript(本文中展示的语言)、Python 或 PHP 等语言那样固有地实现。
最重要的不是照着我演示的模式去实现,而是要能够识别出这种特定模式可以解决的问题,以及何时应该或不应该使用它。这一点至关重要,因为具体的实现方式会因你使用的编程语言而异。
原文发表于https://www.carloscaballero.io,日期为 2019 年 6 月 20 日。
文章来源:https://dev.to/carlillo/understanding-iterator-pattern-in-javascript-typescript-using-symbol-iterator-a19



















