欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ES6生成器的基础

程序员文章站 2022-07-13 08:41:54
...

ES6生成器的基础

JavaScript ES6中最激动人心的新功能之一就是一种称为Generator的新功能。 这个名字有点奇怪,但是乍一看,这种行为似乎很陌生 本文旨在解释它们如何工作的基础知识,并帮助您理解为什么它们对于JS的未来如此强大。

完成运行

在讨论生成器时,要观察的第一件事是,它们在“运行到完成”期望方面与正常功能有何不同。

不管您是否意识到,您始终可以假设一些基本的功能:一旦函数开始运行,它将始终运行完成,然后再运行其他任何JS代码。

例:

setTimeout(function(){
    console.log("Hello World");
},1);

function foo() {
    // NOTE: don't ever do crazy long-running loops like this
    for (var i=0; i<=1E10; i++) {
        console.log(i);
    }
}

foo();
// 0..1E10
// "Hello World"

在这里, for循环将花费相当长的时间,超过一毫秒,但是我们的带有console.log(..)语句的计时器回调无法在运行时中断foo()函数,因此它停留在行的后面(在事件循环上),它耐心等待其转弯。

但是,如果foo()可以被打断怎么办? 那不会在我们的程序中造成破坏吗?

那就是 噩梦 多线程编程面临许多挑战,但是我们非常幸运的是,不必担心这些事情,因为JS始终是单线程的(在任何给定时间仅执行一个命令/函数)。

注意: Web Workers是一种机制,您可以在其中旋转整个单独的线程,以使JS程序的一部分可以在其中运行,而该进程完全与您的主要JS程序线程并行。 这样做不会在我们的程序中引入多线程复杂性的原因是,两个线程只能通过正常的异步事件相互通信,而异步事件始终遵守运行要求的事件循环一次行为。完成。

运行..停止..运行

随着ES6发电机,我们有不同类型的功能,可以在中间,一个或多次被暂停了, 后来恢复了,允许其他代码在这些暂停期间运行。

如果您曾经阅读过有关并发或线程编程的任何文章,那么您可能已经看到了“合作”一词,它基本上表示一个进程(在我们的例子中是一个函数)本身会选择允许中断的时间,以便与其他代码合作 该概念与“抢先式”形成对比,“抢先式”表示过程/功能可能违反其意愿而中断。

ES6生成器功能的并发行为是“合作的”。 在生成器函数体内,您可以使用new yield关键字从自身内部暂停该函数。 没有什么可以使发电机停在外面。 当遇到yield时,它会自行停顿。

但是,一旦生成器暂停了yield ,它就无法自行恢复。 必须使用外部控件重新启动发电机。 我们将在短时间内说明如何发生。

因此,基本上,生成器功能可以停止并重新启动,次数不限。 实际上,您可以指定一个带有无限循环的生成器函数(例如,臭名昭著的while (true) { .. } ),该循环基本上不会结束。 尽管这通常是一个普通的JS程序中的疯狂或错误,但是使用生成器函数,它是完全理智的,有时甚至是您想要执行的操作!

更重要的是,这种停止和启动不仅对发电机功能的执行的控制,但它也使2路的消息传递进出发电机的,因为它的进展。 使用普通函数,您可以在开头获取参数,并在结尾获取return值。 使用生成器功能时,您将以每个yield发送消息,并在每次重新启动时将消息发送回。

语法请!

让我们深入研究这些令人兴奋的新生成器函数的语法。

首先,新的声明语法:

function *foo() {
    // ..
}

注意那里的*吗? 这是新的,看起来有些奇怪。 对于其他语言的人来说,它看起来很像函数的返回值指针。 但是不要感到困惑! 这只是发出特殊生成器功能类型信号的一种方式。

您可能已经看过其他文章/文档,它们使用function* foo(){ }而不是function *foo(){ }*位置不同)。 两者都是有效的,但是我最近决定,我认为function *foo() { }更准确,所以这就是我在这里使用的。

现在,让我们谈谈生成器函数的内容。 在大多数方面,生成器函数只是普通的JS函数。 生成器函数内部几乎没有新语法要学习。

如上所述,我们必须使用的主要新玩具是yield关键字。 yield ___被称为“ yield表达式”(而不是语句),因为当我们重新启动生成器时,我们将返回一个值,而我们发送的任何内容都是该yield ___表达式的计算结果。

例:

function *foo() {
    var x = 1 + (yield "foo");
    console.log(x);
}

当暂停生成器函数时, yield "foo"表达式将发送"foo"字符串值,并且每当(如果有)重启生成器时,发送的任何值都是该表达式的结果,它将然后加到1并分配给x变量。

看到两路通讯? 您发送值"foo"了,暂停自己,在某些点以后 (可能马上,可能是从现在开始很长一段时间!),发电机将重新启动,并会给你一个值返回。 就像yield关键字有点像在要求一个值。

在任何表达式位置,您都可以在表达式/语句中单独使用yield ,并且会假定有一个undefinedyield 所以:

// note: `foo(..)` here is NOT a generator!!
function foo(x) {
    console.log("x: " + x);
}

function *bar() {
    yield; // just pause
    foo( yield ); // pause waiting for a parameter to pass into `foo(..)`
}

生成器迭代器

“生成器迭代器”。 满口,是吗?

迭代器是一种特殊的行为,实际上是一种设计模式,在这种情况下,我们通过调用next()一次遍历一组有序的值。 例如,想像一下在其中具有五个值的数组上使用迭代器: [1,2,3,4,5] 第一个next()调用将返回1 ,第二个next()调用将返回2 ,依此类推。 返回所有值之后, next()将返回nullfalse ,否则将向您发出信号,通知您已遍历数据容器中的所有值。

我们从外部控制生成器功能的方法是构造生成器迭代器并与之交互。 听起来比实际要复杂得多。 考虑这个愚蠢的例子:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
}

要逐步了解该*foo()生成器函数的值,我们需要构造一个迭代器。 我们该怎么做? 简单!

var it = foo();

哦! 因此,以常规方式调用generator函数实际上并不会执行其任何内容。

缠住你的头有点奇怪。 您可能还会想知道,为什么它不是var it = new foo() 耸耸肩。 语法背后的原因很复杂,超出了我们的讨论范围。

因此,现在开始迭代生成器函数,我们只需执行以下操作:

var message = it.next();

这将赋予我们回1yield 1 statment,但是这不是我们回来的唯一的事。

console.log(message); // { value:1, done:false }

实际上,我们实际上是从每个next()调用取回一个对象,该对象具有yield ed-out值的value属性,并且done是一个布尔值,指示生成器函数是否已完全完成。

让我们继续进行迭代:

console.log( it.next() ); // { value:2, done:false }
console.log( it.next() ); // { value:3, done:false }
console.log( it.next() ); // { value:4, done:false }
console.log( it.next() ); // { value:5, done:false }

有趣的是,当我们得到5的值时, done仍然是false 这是因为从技术上讲 ,生成器功能并不完整。 我们仍然必须调用最终的next()调用,如果我们发送一个值,则必须将其设置为yield 5表达式的结果。 只有这样 ,生成器功能才能完成。

因此,现在:

console.log( it.next() ); // { value:undefined, done:true }

因此,生成器函数的最终结果是我们完成了该函数,但是没有给出任何结果(因为我们已经用尽了所有yield ___语句)。

您可能会奇怪,此时,我可以使用生成器函数的return吗,如果可以,该值是否会在value属性中发送出去?

是的

function *foo() {
    yield 1;
    return 2;
}

var it = foo();

console.log( it.next() ); // { value:1, done:false }
console.log( it.next() ); // { value:2, done:true }

... 也没有

依靠生成器的return值可能不是一个好主意,因为当使用for..of循环迭代生成器函数时(请参见下文),最终的return ed值将被丢弃。

为了完整起见,让我们在迭代过程中还考虑将消息同时发送到生成器函数和从生成器函数发送消息:

function *foo(x) {
    var y = 2 * (yield (x + 1));
    var z = yield (y / 3);
    return (x + y + z);
}

var it = foo( 5 );

// note: not sending anything into `next()` here
console.log( it.next() );       // { value:6, done:false }
console.log( it.next( 12 ) );   // { value:8, done:false }
console.log( it.next( 13 ) );   // { value:42, done:true }

您可以看到,仍然可以使用初始foo( 5 )迭代器实例化调用来传递参数(在本例中为x ),就像使用普通函数一样,将x设为5

第一个next(..)调用,我们不发送任何东西。 为什么? 因为没有yield表达式可以接收我们传递的信息。

但是,如果我们确实将值传递给第next(..)调用,则不会发生任何不良情况。 这只是一个被抛弃的价值。 ES6表示,在这种情况下,生成器函数将忽略未使用的值。 注意:在撰写本文时,Chrome和FF都可以使用,但是其他浏览器可能尚未完全兼容,在这种情况下可能会错误地引发错误)。

yield (x + 1)是输出值6 第二个next(12)调用将12发送到该等待的yield (x + 1)表达式,因此y设置为12 * 2 ,值24 然后,随后的yield (y / 3)yield (24 / 3) )发出值8 第三个next(13)调用将13发送到该等待的yield (y / 3)表达式,将z设置为13

最后, return (x + y + z)return (5 + 24 + 13) ,或者42作为最后一个value被返回。

重读几次。 对于大多数人来说,这很奇怪,这是他们几次看到的。

for..of

ES6还通过为运行迭代器完成提供直接支持: for..of循环,从而在语法级别上包含此迭代器模式。

例:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6;
}

for (var v of foo()) {
    console.log( v );
}
// 1 2 3 4 5

console.log( v ); // still `5`, not `6` :(

如您所见,由foo()创建的迭代器会被for..of循环自动捕获,并且会自动为您进行迭代,每个值进行一次迭代,直到done:true 只要donefalse ,它将自动提取value属性并将其分配给您的迭代变量(在本例中为v )。 一旦donetrue ,循环迭代停止(和什么也不做任何最终value返回,如果有的话)。

如上所述,您可以看到for..of循环将忽略并丢弃return 6return 6 另外,由于没有公开的next()调用,因此在需要将值传递给生成器步骤的情况下,不能使用for..of循环,如上所述。

摘要

好的,这就是生成器的基础知识。 如果这仍然有些令人不安,请不要担心。 我们所有人起初都有这种感觉!

很自然地想知道这个新的异国情调的玩具实际上将为您的代码做什么。 但是,他们还有很多 我们只是擦了一下表面。 因此,我们必须更深入地研究,才能发现它们的功能/将有多强大。

你与上面的代码片断发挥各地后(试用Chrome夜间/金丝雀或FF夜间或节点0.11+与--harmony标志),下面的问题可能会出现:

  1. 错误处理如何工作?
  2. 一个发电机可以呼叫另一台发电机吗?
  3. 异步编码如何与生成器一起使用?

这些问题以及更多问题将在此处的后续文章中介绍,请继续关注!

ES6生成器的基础

关于凯尔·辛普森

凯尔·辛普森(Kyle Simpson)是一位开放网络传播者,对JavaScript充满热情。 他是作家,讲习班培训师,技术发言人和OSS贡献者/负责人。

github.com 帖子

翻译自: https://davidwalsh.name/es6-generators