JS生成器:`return yield`和`yield`有什么不同?

38
function* foo() {
  yield 123
};

// - - -

function* foo() {
  return yield 123
};

我似乎无法展示这两者之间的差异。

  • 这两者之间是否有可证明的差异?
  • 在生成器中应该使用 return 吗?

1
感谢您的提问,许多答案帮助我更了解JS生成器。 - yussan
2个回答

47

首先,我要说的是生成器是一个有些复杂的主题,所以在这里提供完整的概述是不可能的。关于更多信息,我强烈推荐 Kyle Simpson 的 You Don't Know JS 系列。第五本书(异步和性能)有关于生成器的详细讨论。

接下来是你提供的具体示例!

首先,你在示例中编写的代码将显示没有区别,但仅当它被正确运行时。以下是一个示例:

function* foo() {
  yield 123;
}

function* bar() {
  return yield 123;
}

var f = foo();
var b = bar();

f.next(); // {value: 123, done: false}
f.next(); // {value: undefined, done: true}
b.next(); // {value: 123, done: false}
b.next(); // {value: undefined, done: true}

如您所见,我没有像普通函数一样调用生成器。生成器本身返回一个生成器对象(迭代器的一种形式)。我们将该迭代器存储在变量中,并使用.next()函数将迭代器推进到下一步(一个yield或return关键字)。
yield关键字允许我们将值传递给生成器,这就是您的示例运行方式不同的地方。这是它的样子:
function* foo() {
  yield 123;
}

function* bar() {
  return yield 123;
}

var f = foo();
var b = bar();

// Start the generator and advance to the first `yield`
f.next(); // {value: 123, done: false}
b.next(); // {value: 123, done: false}

/** Now that I'm at a `yield` statement I can pass a value into the `yield`
 * keyword. There aren't any more `yield` statements in either function,
 * so .next() will look for a return statement or return undefined if one
 * doesn't exist. Like so:
 */
f.next(2); // {value: undefined, done: true}
b.next(2); // {value: 2, done: true}

请注意,f.next(2)返回值为undefined,而b.next(2)返回值为数字2。
这是因为我们将值传递给第二个.next()调用,它被传递到bar()生成器中,并作为yield表达式的结果分配。该表达式出现在return关键字之后;这意味着我们发送到生成器中的值会被返回并结束生成器。 foo()没有显式的返回语句,因此您不会得到返回值(foo()已经产生了它唯一的值,所以生成器已完成,结果是{ done:true, value: undefined })。
注意,迭代器通常不会使用此return,尽管此答案演示了如何手动使用.next()获取返回值。
希望这可以帮助你!

1
写作中存在一些小的逻辑缺陷:bar()没有返回2,而是返回了一个Generator对象(正如您在上面已经指出的那样)。然而,最后的b.next(2)调用返回一个其value属性设置为2的对象。这是因为,在最后一个yield(即最后的暂停/中断)之后调用next()时,它会运行函数直到完成,其最终的value是函数的返回值。 - Domi
1
非常好的例子,谢谢 :) 我采纳了@Domi的反馈,并添加了更多的链接来解释第二个.next()调用是如何将值传递到生成器中的,这些值将被替换为代码中的yield,当代码从yield点恢复执行时...(不是第一个.next()调用;这个值会被忽略,如接受答案的评论、这个问题这里提到的) - Nate Anderson

20

区别在于最后一个连续调用的结果值:

function* fooA() {
  yield 123
};
var a = fooA();
console.log(a.next(1)); // {done:false, value:123}
console.log(a.next(2)); // {done:true,  value:undefined}

function* fooB() {
  return 40 + (yield 123)
};
var b = fooB();
console.log(b.next(1)); // {done:false, value:123}
console.log(b.next(2)); // {done:true,  value:42}

大多数生成器不需要return值,它们的目的是在运行时作为副作用生成一个值流。所有迭代器都属于这种类型,如果它们被for of循环运行,则结果只表示结束,但值会被丢弃。

然而,也有一些生成器需要重视结果值,例如当它们用作描述异步进程的工具时(在async/await promise语法的polyfill中,或者像CSP这样的其他许多情况)。使用yield*在可迭代对象上时,还可以获取return的值。

无论如何,return yield组合起来并不是非常有用。


1
唔...就是不明白为什么最终结果是42而不是41,难道yield执行了两次? - a_a
3
第一个.next(1)的参数被忽略了。最终结果是通过加上402得到的。 - Bergi
1
我明白了!谢谢! - a_a
1
在前面的例子中,它似乎被用于与 yield …; return 相同的效果,确保在暂停之前至少产生一个值。在后面的例子中,yield 似乎被滥用为 await 的意思,在隐式解包承诺的情况下,在 return 之前是不必要的,但有时可以帮助清晰度或类型正确性。 - Bergi
1
@TheRedPea 如果我预料到你会字面理解我的话,我本来就应该写成 if (paused) { yield i; return; } - Bergi
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接