【问题标题】:ES6 generators mechanism - first value passed to next() goes where?ES6 生成器机制 - 传递给 next() 的第一个值在哪里?
【发布时间】:2017-10-30 00:14:00
【问题描述】:

ES6生成器的next()传递参数时,为什么忽略第一个值?更具体地说,为什么这个输出说x = 44而不是x = 43:

function* foo() {
    let i = 0;
    var x = 1 + (yield "foo" + (++i));
    console.log(`x = ${x}`);
}

fooer = foo();

console.log(fooer.next(42));
console.log(fooer.next(43));

// output:
// { value: 'foo1', done: false }
// x = 44
// { value: undefined, done: true }

我对这种生成器行为的心理模型是这样的:

  1. 返回foo1 并在yield 处暂停(并且返回foo1 的next 调用将42 作为参数)
  2. 暂停直到下次呼叫next
  3. 在下一次收益时,继续使用 var x = 1 + 42 行,因为这是之前收到的参数
  4. 打印x = 43
  5. 只需从最后一个 next 返回一个 {done: true},忽略其参数 (43) 并停止。

现在,显然,这不是正在发生的事情。所以... 我在这里做错了什么?

【问题讨论】:

标签: javascript generator yield


【解决方案1】:

我最终编写了这种代码来更彻底地调查这种行为(在重新...阅读MDN docs on generators之后):

function* bar() {
    pp('in bar');
    console.log(`1. ${yield 100}`);
    console.log(`after 1`);
    console.log(`2. ${yield 200}`);
    console.log(`after 2`);
}
let barer = bar();
pp(`1. next:`, barer.next(1));
pp(`--- done with 1 next(1)\n`);
pp(`2. next:`, barer.next(2));
pp(`--- done with 2 next(2)\n`);
pp(`3. next:`, barer.next(3));
pp(`--- done with 3 next(3)\n`);

输出这个:

in bar
1. next: { value: 100, done: false }
--- done with 1 next(1)

1. 2
after 1
2. next: { value: 200, done: false }
--- done with 2 next(2)

2. 3
after 2
3. next: { value: undefined, done: true }
--- done with 3 next(3)

显然正确的心智模型应该是这样的:

  • 在第一次调用next时,生成器函数体运行到yield表达式,yield的“参数”(100第一次)作为返回值返回通过next,生成器主体暂停之前评估yield表达式的值——“之前”部分至关重要

  • 仅在第二次调用 next 时,first yield 表达式的值计算/替换为 this 调用(不是我预期的previous中给出的那个),并且执行一直运行到第二个yield,并且next返回第二个yield的参数值-- 这是我的错误:我假设第一个yield表达式的值是第一次调用next的参数,但它实际上是 第二次调用 next 的参数,或者,换一种说法,它是 调用 next 的参数,在其执行期间,值实际上是计算

这对于发明它的人来说可能更有意义,因为对 next 的调用次数是 yield 语句数量的一倍(还有最后一个返回 { value: undefined, done: true } 以表示终止),所以如果第一次调用的参数不会被忽略,那么最后一次调用的参数将不得不被忽略。此外,在评估 next 的主体时,替换将从其 previous 调用的参数开始。这会更直观,恕我直言,但我认为这也是关于遵循其他语言生成器的约定,并且一致性是最终最好的事情......


题外话但很有启发性: 刚刚尝试在 Python 中进行相同的探索,这显然实现了类似于 Javascript 的生成器,当我尝试将参数传递给第一个时,我立即得到了 TypeError: can't send non-None value to a just-started generator调用next()(明确表明我的心智模型错误!),并且迭代器API也以抛出StopIteration异常结束,所以不需要“额外”next()来检查done是否为真(我想使用最后一个参数的副作用的额外调用只会导致非常难以理解和调试代码......)。比在 JS 中“摸索”要容易得多...

【讨论】:

  • 是的,它就是这样工作的。从之前的next 调用中获取参数是没有意义的,就好像它被缓冲了一样。它不允许立即“响应”传递的值。
  • 鉴于生成器实现来回(或进出)传递“消息”,因此总是需要从某个地方开始。我想通过让第一个产生的值无处可去,也可以有一个比next 调用更多的yield 语句,但在更常见的情况下这只是丑陋的。
  • @Bergi 啊,这是有道理的,我根本没有考虑“响应性”......猜测“对传递给next的参数值的可能反应”如果我这样想的话,是在 actual 调用 next 期间发生的! ...但是在将参数传递给第一次调用 next 时抛出错误,就像我看到的 Python 一样,会帮助像我这样的新手一个 loooot :)
【解决方案2】:

我是从 Axel Rauschmayer's Exploring ES6 那里得到的,尤其是 22.4.1.1。

在收到.next(arg) 时,生成器的第一个操作是将arg 提供给yield。但是在第一次调用.next() 时,没有yield 接收这个,因为它只是在执行结束时。

仅在第二次调用 x = 1 + 43 时执行并随后记录,并且生成器结束。

【讨论】:

  • 那个解释……很有趣。我想我自己最终得到了它,请参阅答案,您的解释和文档对我来说似乎都不够明确:“将arg 提供给yield”或“没有yield 接收这个”甚至意味着什么?!我从评估的正式模型的角度来考虑代码,而“替代模型”(mitpress.mit.edu/sicp/full-text/sicp/book/node10.html)对我来说是最容易理解的,而不是“馈送”、“发送”、“接收”等。.. .这些话对我来说没有多大意义,除非我在谈论多个进程......
【解决方案3】:

我也很难把头绕在生成器上,尤其是在输入依赖于产生值的 if 语句时。尽管如此,if 语句实际上帮助我最终得到了它:

function* foo() {
  const firstYield = yield 1
  console.log('foo', firstYield)

  const secondYield = yield 3
  console.log('foo', secondYield)

  if (firstYield === 2) {
    yield 5
  }
}

const generator = foo()

console.log('next', generator.next( /* Input skipped */ ).value)
console.log('next', generator.next(2).value)
console.log('next', generator.next(4).value)

/* 
  Outputs:

  next 1
  foo 2
  next 3
  foo 4
  next 5    
*/

【讨论】:

    【解决方案4】:

    一旦我意识到这一点,一切都立即变得清晰。

    这是您的典型生成器:

    function* f() {
      let a = yield 1;
      // a === 200
      let b = yield 2;
      // b === 300
    }
    
    let gen = f();
    gen.next(100) // === { value: 1, done: false }
    gen.next(200) // === { value: 2, done: false }
    gen.next(300) // === { value: undefined, done: true }
    

    但这就是实际发生的情况。 让生成器执行任何操作的唯一方法是在其上调用next()。因此需要有一种方法让生成器执行第一个yield之前的代码。

    function* f() {
      // All generators implicitly start with that line
      // v--------<---< 100
           = yield
      // ^-------- your first next call jumps right here
    
      let a = yield 1;
      // a === 200
      let b = yield 2;
      // b === 300
    }
    

    【讨论】:

      猜你喜欢
      • 2018-01-14
      • 2017-02-05
      • 2015-12-03
      • 2018-08-21
      • 1970-01-01
      • 2021-01-11
      • 2016-04-29
      • 2015-04-28
      • 1970-01-01
      相关资源
      最近更新 更多