【Javascript】一起来看看哪些JS中被忽略的特性

JavaScript 是目前最流行的编程语言之一,这一点在许多平台上都有所体现。然而,流行是否意味着它是最先进或最受喜爱的语言呢?它缺少一些在其他语言中被认为是核心特性的东西,比如丰富的标准库、不可变性和宏。但有一个细节,在我看来,没有得到足够的关注——那就是生成器(generators)。

在这篇文章中,我想解释一下迭代器(iterators)和生成器的可能使用场景,以及它们如何让你的代码更加简洁。希望读完这篇文章后,下面这段代码对你来说会变得清晰易懂:

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() 方法的对象都被视为迭代器。next() 方法返回一个包含 value(当前迭代器的值)和 done(表示序列结束的标志)的结构。这个约定在 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);
}
// 输出: 1, 2, 3

如你所见,我们不得不让 done 标志在某个时刻改变,否则循环将是无限的。


生成器

迭代器的下一个进化阶段是生成器的引入。它们提供了一种语法糖,使得迭代器的值可以像函数返回值一样被生成。生成器是一个用 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);
}
// 输出: 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 等)。

希望这篇文章能让你对迭代器和生成器有更深入的理解!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值