迭代器和生成器
根据许多平台(例如 GitHub),JavaScript 是目前最流行的编程语言。然而,流行就等于是最先进或最受喜爱的语言吗?它缺少某些被认为是其他语言不可或缺的组成部分的结构,例如广泛的标准库、不变性和宏。但在我看来,有一个细节没有得到足够的重视——发电机。
在本文中,我想解释迭代器和生成器的可能用例,以及它们如何改进代码的冗长性。我希望,在阅读完本文后,下面的代码片段能够理解所有内容:
while (true) {
const data = yield getNextChunk();
const processed = processData(data);
try {
yield sendProcessedData(processed);
showOkResult();
} catch (err) {
showError();
}
}
这是系列的第一部分:迭代器和生成器。
迭代器
因此,迭代器是一个提供顺序访问数据的接口。
如您所见,该定义没有提及任何有关数据结构或内存的内容。确实,一个空值序列可以表示为一个迭代器而不占用内存空间。
让我们举几个例子:
当您想到迭代器时,您首先想到的可能是数组。它是一种在内存中存储一系列值的数据结构。它也是一个迭代器,因为它提供对其元素的顺序访问。
const arr = [1, 2, 3];
for (const item of arr) {
console.log(item);
}
字符串也是如此。它们作为字符序列存储在内存中,并提供对它们的顺序访问。
const str = 'abc';
for (const char of str) {
console.log(char);
}
让我们看一下下面的函数示例:
const fn = () => Math.random();
这个函数可以被认为是一个迭代器,因为它提供了对随机数的顺序访问。
那么,如果数组(语言中的基本数据结构之一)允许我们按顺序和任意顺序处理数据,那么为什么我们需要迭代器呢?
假设我们需要一个迭代器来实现自然数或斐波那契数列或任何其他无限序列。很难在数组中存储无限序列。我们需要一种机制来逐渐用数据填充数组并删除旧数据,以防止填满进程的整个内存。这种不必要的复杂性增加了额外的实现和维护开销,而无需数组的解决方案只需几行代码即可实现:
const getNaturalRow = () => {
let current = 0;
return () => ++current;
};
迭代器还可用于表示从外部通道(例如 WebSocket)检索的数据。
在 JavaScript 中,任何具有 next() 方法的对象都被视为迭代器,该方法返回一个具有值(当前迭代器值)和完成(指示序列结束的标志)的结构。此约定在ECMAScript 语言标准中进行了描述。这样的对象实现了 Iterator 接口。让我们用这种格式重写前面的例子:
const getNaturalRow = () => {
let current = 0;
return {
next: () => ({ value: ++current, done: false }),
};
};
在 JavaScript 中,还有 Iterable 接口。它表示一个对象,该对象具有返回迭代器的 @@iterator 方法(可通过 Symbol.iterator 常量访问)。可以使用 for..of 循环迭代实现此接口的对象。让我们再次重写我们的示例,这次作为一个 Iterable 实现:
const naturalRowIterator = {
[Symbol.iterator]: () => ({
_current: 0,
next() { return {
value: ++this._current,
done: this._current > 3,
}},
}),
}
for (num of naturalRowIterator) {
console.log(num);
}
// output: 1, 2, 3
如您所见,我们必须让标志“完成”在某个时刻发生变化,否则循环将是无限的。
发电机
迭代器发展的下一个阶段是生成器的引入。它们提供语法糖,允许将迭代器的值作为函数的结果返回。function*
生成器是用星号声明并返回迭代器的函数。迭代器本身并没有明确返回;相反,该函数使用yield
关键字生成迭代器的值。当函数完成执行时,迭代器被视为完成(后续next
方法调用将返回{ done: true, value: undefined }
.
function* naturalRowGenerator() {
let current = 1;
while (current <= 3) {
yield current;
current++;
}
}
for (num of naturalRowGenerator()) {
console.log(num);
}
// output: 1, 2, 3
即使在这个简单的示例中,生成器的主要细微差别也很明显:生成器函数中的代码不会同步执行。next
作为相应迭代器上方法调用的结果,生成器代码的执行是增量发生的。让我们使用前面的示例检查生成器代码是如何执行的。我们将使用一个特殊的光标来标记生成器暂停执行的位置。
在调用 naturalRowGenerator 时,会创建一个迭代器。
function* naturalRowGenerator() {
▷let current = 1;
while (current <= 3) {
yield current;
current++;
}
}
接下来,当我们next
三次调用该方法时,或者在我们的例子中,遍历循环三次时,光标位于 yield 语句之后。
function* naturalRowGenerator() {
let current = 1;
while (current <= 3) {
yield current; ▷
current++;
}
}
并且对于所有后续next
调用,以及退出循环后,生成器完成其执行。后续调用的结果next
将是{ value: undefined, done: true }
。
将参数传递给迭代器
假设我们需要添加功能来重置当前计数器并在我们的自然数迭代器中从头开始计数。
naturalRowIterator.next() // 1
naturalRowIterator.next() // 2
naturalRowIterator.next(true) // 1
naturalRowIterator.next() // 2
很清楚如何在自定义迭代器中处理这样的参数,但是生成器呢?
原来生成器是支持参数传递的!
function* naturalRowGenerator() {
let current = 1;
while (true) {
const reset = yield current;
if (reset) {
current = 1;
} else {
current++;
}
}
}
作为 yield 运算符的结果,传递的参数变得可用。让我们尝试使用游标方法来阐明这一点。在创建迭代器的那一刻,什么都没有改变。现在让我们在第一个next
方法调用后停止:
function* naturalRowGenerator() {
let current = 1;
while (true) {
const reset = ▷yield current;
if (reset) {
current = 1;
} else {
current++;
}
}
}
从 yield 运算符返回后定位游标。在下一次next
调用中,传递给函数的值将设置reset
变量的值。但是第一次next
调用中传递的值会发生什么变化?它无处可去!如果需要将初始值传递给生成器,可以通过生成器的参数来实现。这是一个例子:
function* naturalRowGenerator(start = 1) {
let current = start;
while (true) {
const reset = yield current;
if (reset) {
current = start;
} else {
current++;
}
}
}
const iterator = naturalRowGenerator(10);
iterator.next() // 10
iterator.next() // 11
iterator.next(true) // 10
结论
我们已经探讨了迭代器的概念及其在 JavaScript 中的实现。此外,我们还了解了生成器,这是一种方便地实现迭代器的语法结构。
尽管在本文中,我提供了带有数字序列的示例,但 JavaScript 中的迭代器可以解决范围广泛的任务。它们可以表示任何数据序列甚至许多有限状态机。在下一篇文章中,我想讨论如何使用生成器来构建异步进程(协同程序、goroutines、CSP 等)。
更多前端相关学习教程B站搜索“千锋教育”
- 点赞
- 收藏
- 关注作者
评论(0)