yield* 表达式

如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。

  1. function* foo() {
  2. yield 'a';
  3. yield 'b';
  4. }
  5. function* bar() {
  6. yield 'x';
  7. // 手动遍历 foo()
  8. for (let i of foo()) {
  9. console.log(i);
  10. }
  11. yield 'y';
  12. }
  13. for (let v of bar()){
  14. console.log(v);
  15. }
  16. // x
  17. // a
  18. // b
  19. // y

上面代码中,foobar都是 Generator 函数,在bar里面调用foo,就需要手动遍历foo。如果有多个 Generator 函数嵌套,写起来就非常麻烦。

ES6 提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。

  1. function* bar() {
  2. yield 'x';
  3. yield* foo();
  4. yield 'y';
  5. }
  6. // 等同于
  7. function* bar() {
  8. yield 'x';
  9. yield 'a';
  10. yield 'b';
  11. yield 'y';
  12. }
  13. // 等同于
  14. function* bar() {
  15. yield 'x';
  16. for (let v of foo()) {
  17. yield v;
  18. }
  19. yield 'y';
  20. }
  21. for (let v of bar()){
  22. console.log(v);
  23. }
  24. // "x"
  25. // "a"
  26. // "b"
  27. // "y"

再来看一个对比的例子。

  1. function* inner() {
  2. yield 'hello!';
  3. }
  4. function* outer1() {
  5. yield 'open';
  6. yield inner();
  7. yield 'close';
  8. }
  9. var gen = outer1()
  10. gen.next().value // "open"
  11. gen.next().value // 返回一个遍历器对象
  12. gen.next().value // "close"
  13. function* outer2() {
  14. yield 'open'
  15. yield* inner()
  16. yield 'close'
  17. }
  18. var gen = outer2()
  19. gen.next().value // "open"
  20. gen.next().value // "hello!"
  21. gen.next().value // "close"

上面例子中,outer2使用了yield*outer1没使用。结果就是,outer1返回一个遍历器对象,outer2返回该遍历器对象的内部值。

从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield*表达式。

  1. let delegatedIterator = (function* () {
  2. yield 'Hello!';
  3. yield 'Bye!';
  4. }());
  5. let delegatingIterator = (function* () {
  6. yield 'Greetings!';
  7. yield* delegatedIterator;
  8. yield 'Ok, bye.';
  9. }());
  10. for(let value of delegatingIterator) {
  11. console.log(value);
  12. }
  13. // "Greetings!
  14. // "Hello!"
  15. // "Bye!"
  16. // "Ok, bye."

上面代码中,delegatingIterator是代理者,delegatedIterator是被代理者。由于yield* delegatedIterator语句得到的值,是一个遍历器,所以要用星号表示。运行结果就是使用一个遍历器,遍历了多个 Generator 函数,有递归的效果。

yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。

  1. function* concat(iter1, iter2) {
  2. yield* iter1;
  3. yield* iter2;
  4. }
  5. // 等同于
  6. function* concat(iter1, iter2) {
  7. for (var value of iter1) {
  8. yield value;
  9. }
  10. for (var value of iter2) {
  11. yield value;
  12. }
  13. }

上面代码说明,yield*后面的 Generator 函数(没有return语句时),不过是for...of的一种简写形式,完全可以用后者替代前者。反之,在有return语句时,则需要用var value = yield* iterator的形式获取return语句的值。

如果yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。

  1. function* gen(){
  2. yield* ["a", "b", "c"];
  3. }
  4. gen().next() // { value:"a", done:false }

上面代码中,yield命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。

实际上,任何数据结构只要有 Iterator 接口,就可以被yield*遍历。

  1. let read = (function* () {
  2. yield 'hello';
  3. yield* 'hello';
  4. })();
  5. read.next().value // "hello"
  6. read.next().value // "h"

上面代码中,yield表达式返回整个字符串,yield*语句返回单个字符。因为字符串具有 Iterator 接口,所以被yield*遍历。

如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。

  1. function* foo() {
  2. yield 2;
  3. yield 3;
  4. return "foo";
  5. }
  6. function* bar() {
  7. yield 1;
  8. var v = yield* foo();
  9. console.log("v: " + v);
  10. yield 4;
  11. }
  12. var it = bar();
  13. it.next()
  14. // {value: 1, done: false}
  15. it.next()
  16. // {value: 2, done: false}
  17. it.next()
  18. // {value: 3, done: false}
  19. it.next();
  20. // "v: foo"
  21. // {value: 4, done: false}
  22. it.next()
  23. // {value: undefined, done: true}

上面代码在第四次调用next方法的时候,屏幕上会有输出,这是因为函数fooreturn语句,向函数bar提供了返回值。

再看一个例子。

  1. function* genFuncWithReturn() {
  2. yield 'a';
  3. yield 'b';
  4. return 'The result';
  5. }
  6. function* logReturned(genObj) {
  7. let result = yield* genObj;
  8. console.log(result);
  9. }
  10. [...logReturned(genFuncWithReturn())]
  11. // The result
  12. // 值为 [ 'a', 'b' ]

上面代码中,存在两次遍历。第一次是扩展运算符遍历函数logReturned返回的遍历器对象,第二次是yield*语句遍历函数genFuncWithReturn返回的遍历器对象。这两次遍历的效果是叠加的,最终表现为扩展运算符遍历函数genFuncWithReturn返回的遍历器对象。所以,最后的数据表达式得到的值等于[ 'a', 'b' ]。但是,函数genFuncWithReturnreturn语句的返回值The result,会返回给函数logReturned内部的result变量,因此会有终端输出。

yield*命令可以很方便地取出嵌套数组的所有成员。

  1. function* iterTree(tree) {
  2. if (Array.isArray(tree)) {
  3. for(let i=0; i < tree.length; i++) {
  4. yield* iterTree(tree[i]);
  5. }
  6. } else {
  7. yield tree;
  8. }
  9. }
  10. const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
  11. for(let x of iterTree(tree)) {
  12. console.log(x);
  13. }
  14. // a
  15. // b
  16. // c
  17. // d
  18. // e

由于扩展运算符...默认调用 Iterator 接口,所以上面这个函数也可以用于嵌套数组的平铺。

  1. [...iterTree(tree)] // ["a", "b", "c", "d", "e"]

下面是一个稍微复杂的例子,使用yield*语句遍历完全二叉树。

  1. // 下面是二叉树的构造函数,
  2. // 三个参数分别是左树、当前节点和右树
  3. function Tree(left, label, right) {
  4. this.left = left;
  5. this.label = label;
  6. this.right = right;
  7. }
  8. // 下面是中序(inorder)遍历函数。
  9. // 由于返回的是一个遍历器,所以要用generator函数。
  10. // 函数体内采用递归算法,所以左树和右树要用yield*遍历
  11. function* inorder(t) {
  12. if (t) {
  13. yield* inorder(t.left);
  14. yield t.label;
  15. yield* inorder(t.right);
  16. }
  17. }
  18. // 下面生成二叉树
  19. function make(array) {
  20. // 判断是否为叶节点
  21. if (array.length == 1) return new Tree(null, array[0], null);
  22. return new Tree(make(array[0]), array[1], make(array[2]));
  23. }
  24. let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
  25. // 遍历二叉树
  26. var result = [];
  27. for (let node of inorder(tree)) {
  28. result.push(node);
  29. }
  30. result
  31. // ['a', 'b', 'c', 'd', 'e', 'f', 'g']