简介生成器函数


在〈for…of 与迭代器〉中曾经看过一个range函数的实现,透过迭代器来产生一组值,看来有点复杂,实际上这个需求,在 ES6 中可以透过生成器(Generator)函数来达成:

function* range(start, end) {
    for(let i = start; i < end; i++) {
        yield i;
    }
}

let r = range(3, 8);
for(let n of r) {
    console.log(n);
}

注意到function之后加了个*符号,这表示这会是个生成器函数,只有在生成器函数中,才可以使用yield

就流程来看,range函数首次执行时,使用yield指定一个值,然后回到主流程使用console.log显示该值,接着流程重回range函数yield之后继续执行,循环中再度使用yield指定值,然后又回到主流程使用console.log显示该值,这样的反复流程,会直到range中的for循环结束为止。

显然地,这样的流程有别于函数中使用了return,函数就结束了的情况。实际上,一个生成器函数会返回一个迭代器对象,该对象实现了迭代器的协定,此对象具有next方法:

> function* range(start, end) {
...     for(let i = start; i < end; i++) {
.....           yield i;
.....   }
... }
undefined
> let g = range(2, 5);
undefined
> g.next();
{ value: 2, done: false }
> g.next();
{ value: 3, done: false }
> g.next();
{ value: 4, done: false }
> g.next();
{ value: undefined, done: true }
>

由于生成器本身就是个迭代器,因此for...of实际上是对range返回的迭代器进行迭代,它会调用next方法获取yield的指定值,直到下一个迭代出来的对象其done特性为true为止。因为每次调用迭代器的next时,迭代器才会运算并返回下个产生值,因此就实现惰性求值效果而言,生成器函数的语法非常的方便。

除了以不带实参的方式调用生成器的next方法之外,获取yield的右侧指定值之外,还可以在调用next方法指定实参,令其成为yield的结果,也就是生成器可以给调用者值,调用者也可以指定值给生成器,这成了一种沟通机制。例如,设计一个简单的生产者与消费者程序:

function* producer(n) {
    for(let data = 0; data < n; data++) {
        console.log('生产了:', data);
        yield data;
    }
}

function* consumer(n) {
    for(let i = 0; i < n; i++) {
        let data = yield;
        console.log('消费了:', data);
    }
}

function clerk(n, producer, consumer) {
    console.log(`执行 ${n} 次生产与消费`);
    let p = producer(n);
    let c = consumer(n);
    c.next();
    for(let data of p) {
        c.next(data);
    }
}

clerk(5, producer, consumer);

这个范例程序示范了如何应用生成器与yield,以便在多个流程之间沟通合作。由于next方法若指定实参,会是yield的运算结果,因此clerk流程中必须先使用c.next(),使得流程首次执行至consumer函数中let data = yield处先执行yield,这会令流程回到clerk函数,之后for...of中会调用p.next(),这时流程进行至producer函数的yield data,在clerk获取data之后,接着执行c.next(data),这时流程回到consumer之前let data=yield处,next方法的指定值此时成为yield的结果。一个执行结果如下:

执行 5 次生产与消费
生产了: 0
消费了: 0
生产了: 1
消费了: 1
生产了: 2
消费了: 2
生产了: 3
消费了: 3
生产了: 4
消费了: 4

如果打算创建一个生成器函数,然而数据来源是直接从另一个生成器获取,那会怎么样呢?举例来说,先前的range函数就是返回生成器,而你打算创建一个np_range函数,可以产生指定数字的正负范围,但不包含0:

function* range(start, end) {
    for(let i = start; i < end; i++) {
        yield i;
    }
}

function* np_range(n) {
    for(let i of range(0 - n, 0)) {
        yield i
    }

    for(let i of range(1, n + 1)) {
        yield i
    }
}

for(let i of np_range(3)) {
    console.log(i);
}

因为np_range必须得是个生成器,结果就是得逐一从来源生成器获取数据,再将之yield,像是这边重复使用了for...of来迭代并不方便,你可以直接使用yield*改写如下:

function* range(start, end) {
    for(let i = start; i < end; i++) {
        yield i;
    }
}

function* np_range(n) {
    yield* range(0 - n, 0);
    yield* range(1, n + 1);
}

for(let i of np_range(3)) {
    console.log(i);
}

当需要直接从某个生成器获取数据,以便创建另一个生成器时,yield*可以作为直接衔接的语法。


展开阅读全文