ES6 生成器机制 - 传递给 next() 的第一个值去哪里了?
ES6 generators mechanism - first value passed to next() goes where?
向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 }
我对这种生成器行为的心智模型是这样的:
- return
foo1
并在 yield 处暂停(以及 returns foo1
作为参数 42
的 next
调用)
- 暂停直到下一次调用
next
- 在下一个 yield 继续到带有
var x = 1 + 42
的行,因为这是之前收到的参数
- 打印
x = 43
- 只是 return 来自最后一个
next
的 {done: true}
,忽略它的参数 (43
) 并停止。
现在,显然,这不是正在发生的事情。所以... 我在这里弄错了什么?
我从 Axel Rauschmayer's Exploring ES6 得到这个,尤其是 22.4.1.1.
收到 .next(arg)
后,生成器的第一个动作是将 arg
馈送到 yield
。但是在第一次调用 .next()
时,没有 yield
接收到它,因为它只是在执行结束时。
仅在第二次调用时 x = 1 + 43
被执行并随后被记录,生成器结束。
我最终编写了这种代码以更彻底地调查行为(在重新阅读 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
表达式, "argument" of yield
(100
第一次)作为next
返回的值返回,生成器主体暂停,然后评估该值yield 表达式的 —— "before" 部分至关重要
仅在第二次调用 next
时 first yield
表达式 computed/replaced 的值是在 this 调用中给 next 的参数值(不是我预期的 previous 中给出的那个),并执行 运行s 直到第二个 yield
,并且 next
returns 第二个 yield 的参数值 -- 这是我的错误: 我假定 第一个 yield
表达式 的值是 第一次调用 next
的参数,但它实际上是 第二次调用 next
的参数,或者换句话说,它是 执行期间调用 next
的参数该值实际计算
这对于发明者来说可能更有意义,因为对 next
的调用次数是 yield
语句数量的一倍(还有最后一个返回 { value: undefined, done: true }
信号终止),因此如果第一个调用的参数不会被忽略,那么最后一个调用的参数将不得不被忽略。此外,在评估 next 的主体时,替换将从其 previous 调用的参数开始。这 恕我直言 会更直观,但我认为这也是关于遵循其他语言生成器的约定,最终一致性是最好的事情...
题外话但很有启发性:刚刚尝试在Python中做同样的探索,显然实现了类似于Javascript的生成器,我立即得到了a TypeError: can't send non-None value to a just-started generator
当试图将参数传递给第一次调用 next()
时(明确表示我的心理模型是错误的!),并且迭代器 API 也以抛出 StopIteration
异常,所以不需要 "extra" next()
只需要检查 done
是否为真(我想使用这个额外的调用来产生利用最后一个 next 参数的副作用只会导致 非常难以理解和调试代码...)。 "grok it" 比 JS 更容易...
我也很难全神贯注于生成器,尤其是在根据生成值放入 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
*/
当我意识到这一点时,一切都立刻变得清晰了。
这是您的典型生成器:
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
}
向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 }
我对这种生成器行为的心智模型是这样的:
- return
foo1
并在 yield 处暂停(以及 returnsfoo1
作为参数42
的next
调用) - 暂停直到下一次调用
next
- 在下一个 yield 继续到带有
var x = 1 + 42
的行,因为这是之前收到的参数 - 打印
x = 43
- 只是 return 来自最后一个
next
的{done: true}
,忽略它的参数 (43
) 并停止。
现在,显然,这不是正在发生的事情。所以... 我在这里弄错了什么?
我从 Axel Rauschmayer's Exploring ES6 得到这个,尤其是 22.4.1.1.
收到 .next(arg)
后,生成器的第一个动作是将 arg
馈送到 yield
。但是在第一次调用 .next()
时,没有 yield
接收到它,因为它只是在执行结束时。
仅在第二次调用时 x = 1 + 43
被执行并随后被记录,生成器结束。
我最终编写了这种代码以更彻底地调查行为(在重新阅读 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
表达式, "argument" ofyield
(100
第一次)作为next
返回的值返回,生成器主体暂停,然后评估该值yield 表达式的 —— "before" 部分至关重要仅在第二次调用
next
时 firstyield
表达式 computed/replaced 的值是在 this 调用中给 next 的参数值(不是我预期的 previous 中给出的那个),并执行 运行s 直到第二个yield
,并且next
returns 第二个 yield 的参数值 -- 这是我的错误: 我假定 第一个yield
表达式 的值是 第一次调用next
的参数,但它实际上是 第二次调用next
的参数,或者换句话说,它是 执行期间调用next
的参数该值实际计算
这对于发明者来说可能更有意义,因为对 next
的调用次数是 yield
语句数量的一倍(还有最后一个返回 { value: undefined, done: true }
信号终止),因此如果第一个调用的参数不会被忽略,那么最后一个调用的参数将不得不被忽略。此外,在评估 next 的主体时,替换将从其 previous 调用的参数开始。这 恕我直言 会更直观,但我认为这也是关于遵循其他语言生成器的约定,最终一致性是最好的事情...
题外话但很有启发性:刚刚尝试在Python中做同样的探索,显然实现了类似于Javascript的生成器,我立即得到了a TypeError: can't send non-None value to a just-started generator
当试图将参数传递给第一次调用 next()
时(明确表示我的心理模型是错误的!),并且迭代器 API 也以抛出 StopIteration
异常,所以不需要 "extra" next()
只需要检查 done
是否为真(我想使用这个额外的调用来产生利用最后一个 next 参数的副作用只会导致 非常难以理解和调试代码...)。 "grok it" 比 JS 更容易...
我也很难全神贯注于生成器,尤其是在根据生成值放入 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
*/
当我意识到这一点时,一切都立刻变得清晰了。
这是您的典型生成器:
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
}