21.3 用作迭代器的生成器(数据生产)

在学习本节之前,你应该对 ES6 迭代熟悉,上一章讲解了更多关于迭代的知识。

正如之前讲解的,生成器对象可以是数据生产者,数据消费者或者两者都是。本节将其视为数据生产者,同时实现 IterableIterator 接口(如下所示)。这意味着一个生成器的结果既是一个 Iterable 对象又是一个 Iterator 对象。完整的生成器对象接口将会在后面展示。

interface Iterable {
    [Symbol.iterator]() : Iterator;
}
interface Iterator {
    next() : IteratorResult;
    return?(value? : any) : IteratorResult;
}
interface IteratorResult {
    value : any;
    done : boolean;
}

生成器函数通过 yield 生成一组值,数据消费者通过继承自 Iterator 的方法 next() 来消费这些值。例如,下面的生成器函数生成值 'a''b'

function* genFunc() {
    yield 'a';
    yield 'b';
}

下面的交互展示了如何通过生成器对象 genObj 得到 yield 返回的值:

> let genObj = genFunc();
> genObj.next()
{ value: 'a', done: false }
> genObj.next()
{ value: 'b', done: false }
> genObj.next() // done: true => end of sequence
{ value: undefined, done: true }

21.3.1 迭代一个生成器的方式

由于生成器对象是可迭代的,所以 ES6 中支持迭代的语言结构都可以使用生成器对象。下面三种结构尤其重要。

首先是 for-of 循环:

for (let x of genFunc()) {
    console.log(x);
}
// Output:
// a
// b

其次是扩展操作符(...),将可迭代的序列转换成一个数组的元素(参考关于参数处理的那一章来获取有关这个操作符的更多信息):

let arr = [...genFunc()]; // ['a', 'b']

第三个,解构:

> let [x, y] = genFunc();
> x
'a'
> y
'b'

21.3.2 从生成器返回

前面的生成器函数没有包含一个显示的 return 。隐式的 return 等价于返回 undefined 。让我们看看带有显示 return 的生成器:

function* genFuncWithReturn() {
    yield 'a';
    yield 'b';
    return 'result';
}

next() 返回的最后一个对象包含了返回值,并且对象的属性 donetrue

> let genObjWithReturn = genFuncWithReturn();
> genObjWithReturn.next()
{ value: 'a', done: false }
> genObjWithReturn.next()
{ value: 'b', done: false }
> genObjWithReturn.next()
{ value: 'result', done: true }

但是,大多数做迭代的语法结构都忽略 done 属性里面的值:

for (let x of genFuncWithReturn()) {
    console.log(x);
}
// Output:
// a
// b

let arr = [...genFuncWithReturn()]; // ['a', 'b']

yield* 是一个执行生成器嵌套调用的操作符,它会考虑 done 属性里面的值,这在后面讲解。

21.3.3 示例:迭代属性

让我们看一个示例,该例子展示了生成器实现迭代功能是多么的方便。下面的函数, objectEntries() ,返回一个对象属性的迭代器:

function* objectEntries(obj) {
    // In ES6, you can use strings or symbols as property keys,
    // Reflect.ownKeys() retrieves both
    let propKeys = Reflect.ownKeys(obj);

    for (let propKey of propKeys) {
        yield [propKey, obj[propKey]];
    }
}

该函数使你能够通过 for-of 循环迭代对象 jane 的属性:

let jane = { first: 'Jane', last: 'Doe' };
for (let [key,value] of objectEntries(jane)) {
    console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe

为了作为对比 - 一个不使用生成器的 objectEntries() 实现要复杂很多:

function objectEntries(obj) {
    let index = 0;
    let propKeys = Reflect.ownKeys(obj);

    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (index < propKeys.length) {
                let key = propKeys[index];
                index++;
                return { value: [key, obj[key]] };
            } else {
                return { done: true };
            }
        }
    };
}

21.3.4 只能在生成器中使用 yield

一个值得注意的生成器的限制是只能在生成器函数中使用 yield 。也就是说,在回调函数中使用 yield 是没有效果的:

function* genFunc() {
    ['a', 'b'].forEach(x => yield x); // SyntaxError
}

不能在非生成器函数中使用 yield ,这就是为什么前面的代码引起了语法错误。在此种场景下,把代码改成不使用回调函数的形式是很容易的(如下所示)。但是不幸的是这不是总会奏效的。

function* genFunc() {
    for (let x of ['a', 'b']) {
        yield x; // OK
    }
}

上面的限制在后面讲解:它们使生成器更容易实现,并且兼容事件循环。

21.3.5 通过 yield* 嵌套(用于输出)

只能在生成器函数中使用 yield 。因此,如果想实现一个生成器的嵌套逻辑,需要一种从其它生成器中调用某一个生成器的方式。本节会展示这种方式,实际上这比听起来的要复杂,这就是为什么 ES6 有一个特殊的操作符 yield* 。现在,我只解释在两个生成器都有输出的时候 yield* 是如何工作的,我会在后面讲解如果包含输入的话优势如何工作的。

一个生成器是如何嵌套调用另一个生成器的?假设已经有一个生成器函数 foo

function* foo() {
    yield 'a';
    yield 'b';
}

如何在另一个生成器函数 bar 中调用 foo ?下面的方式是不顶用的!

function* bar() {
    yield 'x';
    foo(); // does nothing!
    yield 'y';
}

调用 foo() 返回一个对象,但是实际上并不会执行 foo() 。这就是为什么 ECMAScript 6 有操作符 yield* ,该操作符用于生成器的嵌套调用:

function* bar() {
    yield 'x';
    yield* foo();
    yield 'y';
}

// Collect all values yielded by bar() in an array
let arr = [...bar()];
    // ['x', 'a', 'b', 'y']

在内部, yield* 类似于:

function* bar() {
    yield 'x';
    for (let value of foo()) {
        yield value;
    }
    yield 'y';
}

yield* 的操作数不一定是生成器对象,也可以是可迭代的对象:

function* bla() {
    yield 'sequence';
    yield* ['of', 'yielded'];
    yield 'values';
}

let arr = [...bla()];
    // ['sequence', 'of', 'yielded', 'values']

21.3.5.1 用 yield* 拿到最后一个值

大多数支持迭代的语法结构都会忽略迭代的最后一个对象( done 属性为 true )。生成器通过 return 返回最后一个值。 yield* 表达式的值就是迭代的最后一个对象:

function* genFuncWithReturn() {
    yield 'a';
    yield 'b';
    return 'The result';
}
function* logReturned(genObj) {
    let result = yield* genObj;
    console.log(result); // (A)
}

如果想执行到行 A ,首先就要迭代掉 logReturned() 中生成的所有值:

> [...logReturned(genFuncWithReturn())]
The result
[ 'a', 'b' ]

21.3.5.2 遍历树

用递归的方式遍历一棵树是很简单的,用传统的方式给一颗树添加一个迭代器是很复杂的。这就是为什么生成器在此处大放异彩:可以通过递归实现一个迭代器。作为一个例子,考虑下面的二叉树的数据结构。它是可迭代的,因为有一个键为 Symbol.iterator 的方法,该方法是一个迭代器方法,在调用的时候返回一个迭代器。

class BinaryTree {
    constructor(value, left=null, right=null) {
        this.value = value;
        this.left = left;
        this.right = right;
    }

    /** Prefix iteration */
    * [Symbol.iterator]() {
        yield this.value;
        if (this.left) {
            yield* this.left;
        }
        if (this.right) {
            yield* this.right;
        }
    }
}

下面的代码创建了一颗二叉树,并通过 for-of 循环遍历:

let tree = new BinaryTree('a',
    new BinaryTree('b',
        new BinaryTree('c'),
        new BinaryTree('d')),
    new BinaryTree('e'));

for (let x of tree) {
    console.log(x);
}
// Output:
// a
// b
// c
// d
// e

results matching ""

    No results matching ""