原文:Yield! Yield! How Generators work in JavaScript,作者:Ashay Mandwarya

如果标题的提示还不够清晰的话,我就明说了,在这篇文章中我们将讨论生成器。

在进入正题之前,我们先复习一下函数基础:

  • 在JavaScript中,函数是执行某项任务的一组语句,并且在结尾会返回某个值。
  • 如果你重复调用一个函数,函数会重复执行所有语句。
  • 箭一旦脱弓就再也收不回来了,要么击中要么错过目标。被调用的函数也一样,要么运行并返回一个值,要么抛出一个错误然后停止所有语句的执行。

想要理解生成器,你必须要牢记这三点。

生成器

生成器是一种特殊的函数,可以在函数执行途中停下来,之后再在同一个地方继续执行。生成器是迭代器和函数的结合。现在看这句话可能让你觉得困惑,但我保证读完这篇文章你就会豁然开朗。

打一个比方,你和妈妈正在玩游戏,突然妈妈有一些事要忙,你们停下游戏,帮妈妈完成工作,然后继续游戏,这个感觉就和生成器一样。

迭代器是一个对象,它定义了一个序列,并在其终止时可能有一个返回值。 — MDN。

迭代器本身也是一个内容丰富的话题,本文不做展开

基本语法

生成器的语法类似函数,但是多了一个星号(*)。

function* name(arguments) { statements }

name — 函数(生成器)名

arguments — 函数(生成器)的参数

statements — 函数(生成器)体

返回

函数返回任意值:一个值、一个对象或者函数本身。生成器函数返回一个特殊的对象叫做生成器对象(不完全对)。这个对象如下面的代码片段:

译者注:之所以说返回一个生成器对象不完全对,是因为准确来讲,首次调用生成器函数的时候返回Generator迭代器,然后通过调用生成器下一个方法消耗值时,Generator函数执行,直到遇到yield关键字。详情可以查看MDN

{value: value,  done: true|false}

这个对象有两个属性 valuedonevalue包含了被yield关键字控制的值。done包含了一个**布尔值(true|false)**告诉生成器 .next()是否生成下一值或者是undefined

上述内容比较难以理解,让我们通过例子来解释:

2GKCXYgOAdydbo5qaKeXPayVwUXMpDPPzY1p

function* generator(e) {  
    yield e + 10;  
    yield e + 25;  
    yield e + 33;
    }
    var generate = generator(27);
console.log(generate.next().value); // 37
console.log(generate.next().value); // 52
console.log(generate.next().value); // 60
console.log(generate.next().value); // undefined

让我们逐行理解上面的代码运行机制:

第1–5行: 在第1-5行我们定义了一个名为generator的生成器并有一个形参e,在这个函数体中包含了被关键字yield控制的语句以及运算。

第6行: 在第六行中,我们将生成器赋值给一个变量,变量名为generate

第8–11行: 这里调用了一堆console.log分别调用生成器对应的next方法,这个方法会调用生成器对象的value属性。

cafqdPSIaj55dp6A5GaErDFtrS0LueYhf87K

当生成器被调用的时候,它并不像普通函数一样立刻被执行,取而代之的是返回一个迭代器(这就是为什么生成器会使用*,告诉JS要返回一个迭代器对象). 当迭代器的next()方法被调用, 生成器开始执行直到遇到带有yield关键字的语句。碰到yield之后,返回生成器对象,这个部分上文已经详细介绍过了。 再次调用next()方法会重新开始执行生成器直到遇到下一个yield语句,不断反复直到没有任何yield关键字。

b8YEAKz8FN0BiZTL-9nWtahqnChK1A0dcnIa

到这一步之后如果再次调用next方法会返回值为undefined的生成器对象。

现在让我们对这个生成器例子稍作修改,在内部添加一个return语句:

cwoYBqzWffwSM5RCEJAUzpsPs-U39zfe6EV1

在生成器中的中的return语句和其他所有函数中的一样,会停止函数执行。一旦遇到return关键字,生成器对象的done属性会被设置为truereturn的值会被设为生成器对象的value属性。之后其他所有yield关键字会返回undefined

如果抛出了一个错误,生成器的执行也会停止。

VtjqhfNkczhAnzaY7BmLV1Y7fLs0Du3cVzQP

如果要yield一个生成器,我们需要在yield关键字后添加一个 * 来告诉JS我们要在内部执行一个生成器。 [yield*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield*)被分配给另一个生成器函数之后所有generator2中的值都可以通过原函数中的generate.next()来生成。在控制台中第一个 yield来自第一个生成器,后面两个来自第二个生成器,只不过是被第一个生成器生成的。

优点

懒加载

懒加载指的是仅在需要的时候做价值评估。正如我们会在接下来的例子中看到的,我们可以使用生成器来实现懒加载。我们可以在需要的时候生成值,而不是同时执行所有语句。

下面的例子会生成无限多个随机数字。我们可以尽情调用 next()在我们需要的时候生成我们想要的随机数。

0uRO5-e1uAChjJR9-m21IhjV46cZ7tzH10Xl

function * randomize() {  
    while (true) {
        let random = Math.floor(Math.random()*1000);
            yield random;  
            }
        }
var random= randomize();
console.log(random.next().value)

内存效率

从上面这个例子中我们可以看出生成器非常省内存,因为我们只按需生成值,所以就不需要太多内存。

陷阱

生成器虽然有用但是也有一些陷阱:

  • 生成器不可以被随机访问 和数组以及其他一些数据结构一样,生成器中的值必须一个接一个的生成,不能随机访问其中某一个元素。
  • 生成器只提供一次性访问 生成器不允许反复遍历值,一旦访问完所有的值就必须创造新的生成器实例来重新遍历值。

为什么需要生成器?

在JS中生成器被广泛采用,让我们来创造自己的生成器。

实现生成器

一个迭代器使得程序员可以遍历容器对象 ——维基百科

让我们使用迭代器打印字符串中所有字母。字符串本身也是迭代器。

迭代器

jxlJeu0eRnrbnVoQQiUnQWuW6WRqy6lewlui

const string = 'abcde';
const iterator = string[Symbol.iterator]();
console.log(iterator.next().value) //a
console.log(iterator.next().value) //b
console.log(iterator.next().value) //c
console.log(iterator.next().value) //d
console.log(iterator.next().value) //e

我们也可以使用生成器完成上述操作:

BLoMgkxRn2Um8XnncvONthkIzSnwnDtLZxtd

function * iterator() {
    yield 'a';
    yield 'b';
    yield 'c';
    yield 'd';
    yield 'e';
    }
    for (let x of iterator()) {
       console.log(x);
       }
//a
//b
//c
//d
//e

对比上述两种方法,我们能够发现生成器避免了混乱。我知道上述例子不够完美,但起码证明了:

  • 在生成器中不需要执行 next()
  • 在生成器中不需要[Symbol.iterator]()调用
  • 在一些情况下,如果使用迭代器我们甚至需要设置object.done属性来返回true/false。

Async-Await ~ Promises+生成器

你可以阅读我之前写的文章了解async/await, 也可以阅读这篇文章了解Promise。

简言之,Async/Await是promise和生成器的结合。

Async-Await

async function asyncAwait(){
    let a = await(task1);
    console.log(a);
    let b = await(task2);
    console.log(b);
    let c = await(task3);
    console.log(c);
}

Promises+Generators

function * generatorPromise(){
    let a = yield Promise1();
    console.log(a);
    let b = yield Promise2();
    console.log(b);
    let c = yield Promise3();
    console.log(c);
}

正如我们估计的那样,两者的输出结果一致。这正是因为async/await的机制就是基于生成器和promise的结合。async/await本身包含更多的内容,但是上面的例子我们只展现其中一小部分。

无限数据结构

标题可能有点让人困惑,但是我们确实可以通过while循环来创建一个无限生成值的生成器:

ximCs8aJ4EUtQfo8AAPza6eRK9IAUd70QLCN

function * randomize() { 
     while (true) {
        let random = Math.floor(Math.random()*1000);
            yield random;  
            }
        }
    var random= randomize();
    while(true)
    console.log(random.next().value)

在上述代码中,我们创建了一个无限生成器。每次调用next()方法都会生成一个随机数。这个生成器可以生成无限的随机数,这就是一个非常基础的例子。

总结

关于生成器的内容非常多,我们在这篇文章中只介绍了基础。希望你喜欢这篇文章并从中学习到新的知识。

关注我,鼓掌!

dagqbne49wWylj3wlhZWGKij2pXISMlkKyn6