能否将 ECMAScript 6 生成器重置为初始状态?

40

在提供的(非常简单的)生成器中,是否可以将生成器返回到其原始状态以便再次使用?

var generator = function*() {
    yield 1;
    yield 2;
    yield 3;
};

var iterable = generator();

for (let x of iterable) {
    console.log(x);
}

// At this point, iterable is consumed.
// Is there a method for moving iterable back
// to the start point by only without re-calling generator(),
// (or possibly by re-calling generator(), only by using prototype 
//  or constructor methods available within the iterable object)
// so the following code would work again?

for (let x of iterable) {
    console.log(x);
}

我希望能够将可迭代对象传递给其他作用域,在其上进行迭代,进行一些其他操作,然后在同一作用域中稍后再次对其进行迭代。


1
不符合当前的草案:https://people.mozilla.org/~jorendorff/es6-draft.html#sec-iterator-interface - Felix Kling
2
顺便问一下,for (let x in iterable) 不应该是 for (let x of iterable) 吗? - Felix Kling
是的,你说得对 -- 我会更新代码。 - dvlsg
1
作为给其他读者的提示,我目前正在使用以下解决方法(不一定是解决方案)。通过在生成器函数原型上创建一个@@iterator属性,在访问时自动执行它,返回底层生成器的@@iterator,我可以基本上循环遍历生成器函数。最终结果是我可以按预期迭代生成器函数,使其自动为我创建底层生成器。至于为什么ES6默认情况下不包括此功能,我不确定... - dvlsg
迭代器具有状态,而生成器则没有。您调用生成器以提供迭代器,一旦迭代器被消耗,您再次调用生成器以进行迭代。 - rvighne
当然。我最初的愿望(几年前)是能够像迭代其他可迭代对象(数组、集合、映射等)一样迭代生成器,其中你可以在 for (let val of iterable) {} 中使用生成器。最终,我创建了自己的包装类,在其上定义了 Symbol.iterator,它会在尝试迭代之前自动展开任何包装的生成器(只要不需要任何参数)。这个方法很好用。但是将生成器视为惰性加载的数据序列并不利于性能。 - dvlsg
11个回答

29

如果您的意图是在某个范围内进行迭代、执行其他操作,然后能够稍后在同一范围内再次进行迭代。

那么您唯一不应尝试做的事情就是传递迭代器,而是传递生成器:

var generator = function*() {
    yield 1;
    yield 2;
    yield 3;
};

var user = function(generator){

    for (let x of generator()) {
        console.log(x);
    }

    for (let x of generator()) {
        console.log(x);
    }
}

或者只需创建一个“轮询”迭代器并在迭代时进行检查

var generator = function*() {
    while(true){
        yield 1;
        yield 2;
        yield 3;
    }
};

for( x in i ){
    console.log(x);
    if(x === 3){
        break;
    }
}

将while循环放在生成器内部是非常聪明的,这帮助了我很多。谢谢! - rom
我爱你,它起作用了! - Nelson Javier Avila

18
在此时,可迭代对象已被消耗,也就是说其内部的[[GeneratorState]]状态为completed
有没有一种方法可以将可迭代对象移回起始点而不必重新调用generator()?
没有。规范说明:
“一旦生成器进入“completed”状态,它永远不会离开它,并且与之关联的执行上下文永远不会恢复。可以在这一点上丢弃与生成器相关的任何执行状态。”

可能通过重新调用generator()函数,也可以使用可迭代对象内部提供的原型或构造器方法来实现。否。虽然规范中没有明确说明,但可迭代对象上没有比[[GeneratorState]]和[[GeneratorContext]]更多的实例特定属性可用。然而,信息性的"Generator Object Relationships" grapic图表指出:“每个生成器函数都有一个相关联的原型,该原型没有构造函数属性。因此,生成器实例不会公开访问其生成器函数。”

我希望你能将可迭代对象传递到其他范围,可以传递生成器函数或产生新的生成器实例的内容。请参考以下翻译:

I would like to be able to pass the iterable off to some other scope

传递生成器函数,或者产生新的生成器实例。


我认为这在当前规格下是有意义的。感谢您的报价和建议。 - dvlsg

7
每当你需要“重置”一个迭代器时,只需丢弃旧的迭代器并创建一个新的即可。

var generator = function*() {
    yield 1;
    yield 2;
    yield 3;
};
const makeIterable = () => generator()

for (let x of makeIterable()) {
    console.log(x);
}

// At this point, iterable is consumed.
// Is there a method for moving iterable back
// to the start point by only without re-calling generator(),
// (or possibly by re-calling generator(), only by using prototype 
//  or constructor methods available within the iterable object)
// so the following code would work again?

for (let x of makeIterable()) {
    console.log(x);
}


我认为这应该是答案。 - KWallace

5
据我了解,这似乎是不可能的。根据这个有用的wiki和关于生成器的ES6草案中generatorstart部分所述,一旦您从其中返回(而不是yield),它就会被放置在“closed”状态,并且没有办法将其移回到“newborn”状态,这是新生成器的初始状态。
您可能需要向其他作用域传递一个回调来创建新的生成器。作为解决方法,如果您想要,甚至可以将该回调作为您发送到其他作用域的生成器上的自定义方法,该回调将创建另一个作用域的新生成器。
如果您考虑生成器的工作原理,他们必须重新执行以重置其初始状态,而且没有理由支持这样做。这类似于询问为什么不能重新执行现有对象的构造函数并期望在相同对象中拥有一个原始对象。尽管这些都是技术可行的,但很麻烦,难以正常工作,而且真的没有理由支持它。如果您想要一个原始对象,只需创建一个新对象。生成器也是如此。
这有点hacky,但值得思考。您可以创建一个重复自己的生成器。假设您的生成器像这样工作:
var generator = function*() {
    while (true) {
        yield 1;
        yield 2;
        yield 3;
        yield null;
    }
};

var iterable = generator();

for (let x of iterable) {
    if (x === null) break;
    console.log(x);
}

// generator is now in a state ready to repeat again

我可以很容易地看出这可能是一种反模式,因为如果你曾经这样做过:

for (let x of iterable) {
    console.log(x);
}

你将会得到一个无限循环,因此必须非常小心地使用。值得一提的是,上述维基展示了无限斐波那契数列的例子,因此无限生成器肯定是可以被考虑的。


那是一个有趣的想法。不过我认为在我的情况下它行不通,因为我无法访问修改原始生成器的权限(我正在考虑扩展我的库,其中包含数组操作,以便也可以处理生成器操作)。 - dvlsg
@dvlsg - 你可以包装一个迭代器,使其具有.restart()方法或类似的功能。你的.restart()方法将只是从你的库中创建一个新的迭代器,然后在外部迭代器的下一次.next()调用时,将使用新的内部迭代器重新开始。这里有很多可能性,可以让远程作用域重新开始。 - jfriend00
我正在考虑采取这种方式。希望如此。我遇到的问题之一是能否将一个生成器的方法链接到另一个生成器 -- 类似于C# Linq,您可以调用MyEnumerable.Where(x => x.id > 3).Select(x => x.value); 但是!那是另一天的问题。 - dvlsg

5
根据ES6的草案版本
一旦生成器进入“完成”状态,它就永远不会离开并且与之关联的执行上下文也永远不会恢复。此时可以丢弃与生成器相关的任何执行状态。
因此,一旦完成,就没有重置它的方法。这也是有道理的。我们称其为生成器,有其原因 :)

糟糕。我想我正在追求C#的树(这是我习惯的), 在那里您可以在同一IEnumerable上执行foreach循环而无需重新初始化它。我想这实际上是在内部调用GetEnumerator(),这本质上是重新创建迭代器。 - dvlsg
假设指针在六项中的第4项。还没有完成,对吧..?我猜那是可能的吧?;) - Jack Fuchs

4
  • 您可以将可迭代对象的.next()方法传入一个可选参数,用于重置生成器的状态。

  • yield不仅产生当前生成器可迭代的状态,还会查找传递给生成器的状态。

  • 因此,回答您的问题,是的,只要生成器没有完成(done仍为false),就可以将其重置为初始状态。在您的情况下,您需要将生成器代码更改为以下内容:

let generator = function* () {
  let count = 0;
  while (true) {
    count += 1;
    /*
    if you pass a paramater of value `true` to the iterable's next,
    the `yield` will be equal to true.
    Thus `reset` will be true and the if condition below will executed.
    Like that the yielded value `count` will be reset to its initial value of 0.
    */
    let reset = yield count;
    if (reset) {
      count = 0;
    }
  }
}

let iterable = generator();

console.log(iterable.next().value);  // 1
console.log(iterable.next().value);  // 2
console.log(iterable.next().value);  // 3
console.log(iterable.next().value);  // 4
console.log(iterable.next(true).value);  // 1 as you can see the count has been reset
console.log(iterable.next().value);  // 2
console.log(iterable.next().value); // 3


3
您也可以通过以下方式重置可迭代对象的生成器:
let iterable = generator();

function* generator(){
    yield 1;
    yield 2;
    yield 3;
    iterable = generator();
}

for (let x of iterable) {
    console.log(x);
}

//Now the generator has reset the iterable and the iterable is ready to go again.

for (let x of iterable) {
    console.log(x);
}

我个人不知道这样做的利弊。但是,只要在生成器完成时重新分配可迭代对象,它就像您预期的那样工作。

编辑:通过更多了解此方法的工作原理,建议只使用像Azder展示的生成器:

const generator = function*(){
    yield 1;
    yield 2;
    yield 3;
}

for (let x of generator()) {
    console.log(x);
}

for (let x of generator()) {
    console.log(x);
}

我推荐的版本将防止你在迭代过程中遇到问题时无法继续运行...例如,如果你在等待一个url调用另一个url,如果第一个url失败,你必须刷新应用程序才能再次尝试第一个yield。


1

我认为这不是生成器的问题,而是迭代器的问题 - 实际上是“做工”的问题。要重置迭代,您只需要创建一个新的迭代器。我可能会使用类似于以下的愚蠢高阶函数:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
}

const iterateFromStart = (func) => {
    // every invocation creates a brand new iterator   
    const iterator = func();
    for (let val of iterator) {
        console.log(val)
  }
}

iterateFromStart(foo); // 1 2 3
iterateFromStart(foo); // 1 2 3

1

根据该主题下其他人的回答,简短的答案是坚决不行。您无法将迭代器重置为其初始状态,尤其是由生成器产生的迭代器。根据您处理的生成器类型,再次调用生成器以给您一个新的迭代器甚至可能会产生完全不同的结果。

长答案是... 是的,但不完全是。您需要在传递结果之前迭代一次,并将结果缓存到数组中(从而失去了生成器的性能优势),或者您需要将其包装在类实例中,该类实例将处理内部状态并缓存来自迭代器的结果。

这里是一个示例实现,我创建了一个类似于数组的列表式类,允许我通过缓存结果并允许多个调用来像使用数组一样传递迭代器,同时仍返回每个结果的相同状态和位置。

https://github.com/ahuggins-nhs/js-edi/blob/element-selectors/packages/dom/query/QueryEngineList.ts


1
如果你想要返回数组的值并回到第一个项目(在最后一个项目被返回后),你可以像这样做:
function * arrayIterator () {
    let i = 0
    while ( i < array.length) {
        yield array[i]
        if (i === array.length - 1) i = -1 //this will reset the loop to 0 with i++
        i++
    }
}



如果您想重置一个生成器,可以再次初始化它。

let gen = arrayIterator

假设您想要返回特定的值。之后,您需要像这样重置生成器:

const newValue = gen.return("I was returned").value

gen = arrayIterator() // now you are back at the start, when you call gen.next().value


更新: 我找到了一个更简单的解决方案:

function * arrayIterator(){
    yield* array
}
let gen = arrayIterator()

yield* 将下一个数组项返回,就像它是自己的生成器一样。

然后,您可以像这样重置迭代:

let {value, done} = gen.next()
if (done === true) {
    gen = arrayIterator()
    value = gen.next().value // value at position 0
}

1
请注意,fen函数也可以替换为gen = array Symbol.iterator,使其更加简单。 - J0hannes

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