迭代器和发电机
#javascript #初学者 #编程 #generators
根据许多平台(例如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 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等)。