在本文中,我想解释迭代器和发电机的可能用例,以及它们如何改善代码的详细性。我希望,在阅读本文后,以下代码将使一切都有意义:
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 language standard中描述。这样的对象实现了迭代器接口。让我们以这种格式重写上一个示例:
const getNaturalRow = () => {
let current = 0;
return {
next: () => ({ value: ++current, done: false }),
};
};
在JavaScript中,还有峰值接口。它代表具有@@迭代方法(可通过符号。列表常数访问)的对象,该对象返回迭代器。可以使用for for循环迭代实现此接口的对象。让我们再次重写我们的示例,这一次是一个可迭代的实现:
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
方法三次,或者在我们的情况下,通过循环进行三次迭代时,光标是在收益率语句之后定位的。
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++;
}
}
}
传递的参数作为收益率运算符的结果可用。让我们尝试使用光标方法来澄清这一点。在创建迭代器的那一刻,什么都没有改变。现在让我们在第一个next
方法呼叫之后停止:
function* naturalRowGenerator() {
let current = 1;
while (true) {
const reset = ▷yield current;
if (reset) {
current = 1;
} else {
current++;
}
}
}
从收益率运算符返回后,将放置光标。使用下一个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中的迭代器可以解决广泛的任务。它们可以代表任何数据序列,甚至代表许多有限状态机器。在下一篇文章中,我想讨论如何使用发电机来构建异步过程(Coroutines,Goroutines,CSP等)。