【问题标题】:Is it possible to reset an ECMAScript 6 generator to its initial state?是否可以将 ECMAScript 6 生成器重置为其初始状态?
【发布时间】:2014-07-13 22:16:24
【问题描述】:

鉴于提供的(非常简单的)生成器,是否可以将生成器恢复到其原始状态以再次使用?

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);
}

我希望能够将可迭代对象传递给其他范围,对其进行迭代,做一些其他事情,然后稍后在同一范围内再次对其进行迭代。

【问题讨论】:

  • 不按照目前的草案:people.mozilla.org/~jorendorff/…
  • 顺便说一句,for (let x in iterable) 不应该是 for (let x of iterable) 吗?
  • 是的,你是对的——我会更新代码。
  • 作为其他阅读本文的人的说明,我目前正在使用以下解决方法(不一定是解决方案)。通过在生成器函数原型上创建一个@@iterator 属性,该属性在访问时自动执行,返回底层生成器的@@iterator,我基本上可以循环遍历生成器函数。最终结果是我可以根据需要迭代生成器函数,允许它自动为我创建底层生成器。至于为什么 ES6 默认不包含这个功能,我不确定……
  • 迭代器有状态,而不是生成器。你调用生成器给你一个迭代器,一旦它被消耗,你再次调用生成器再次迭代。

标签: javascript generator ecmascript-harmony ecmascript-6


【解决方案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

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

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
}

【讨论】:

  • 注意,fen函数也可以换成:gen = array[Symbol.iterator](),更简单
【解决方案2】:
  • 您可以向生成器的 .next() 方法传递一个可选参数,您可以使用该参数来重置生成器的状态。

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

  • 所以要回答您的问题,是的,您可以将生成器重置为其初始状态,只要它没有完成(完成仍然是错误的)。在您的情况下,您必须将生成器代码更改为以下内容:

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】:

    对于这个帖子上的其他人来说,简短的回答是“不”。您不能将迭代器重置为其初始状态,尤其是由生成器生成的状态。根据您正在处理的生成器,再次调用生成器以给您一个新的迭代器甚至可能会产生一组完全不同的结果。

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

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

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

    【讨论】:

      【解决方案4】:

      不,不会回到原来的状态。

      为了说清楚,你必须了解生成器函数的工作原理。

      当第一次调用生成器函数时,它返回迭代器(作为它的整个主体)。 此返回迭代器的初始状态存储在其变量中。两个非常重要的变量是 发电机状态, 发电机位置。

      还有其他变量如GeneratorFunction、GeneratorReceiver、Scopes。可以忽略哪个来理解这个答案。

      所以初始状态将是, 发电机状态:暂停。 发电机位置:1;

      现在要使用迭代器,您应该使用 .next(); 调用它。 因此,迭代器将从“GeneratorLocation”指向的位置恢复执行

      现在生成器会将其 GeneratorLocation 的值更新为第一次产生结果的第 no 行,并且 GeneratorLocation 将保持不变,直到它返回最后一个产量。

      现在对于 .next 的每次连续调用,Generator 将从 GeneratorLocation 的值开始执行,而不是从头开始。

      因此,除非您在生成器函数中重复代码,否则无法重置为初始状态。最好的解决方案是使用参数重新创建新的迭代器。

      【讨论】:

        【解决方案5】:

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

        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
        

        【讨论】:

          【解决方案6】:

          你也可以让你的生成器像这样重置你的迭代:

          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 Showed 这样的生成器:

          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 失败,您必须将您的应用刷新到它才能再次尝试第一个 yield。

          【讨论】:

            【解决方案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);
            }

            【讨论】:

              【解决方案8】:

              如果你的意图是

              到其他范围,对其进行迭代,做一些其他的事情,然后能够稍后在同一范围内再次对其进行迭代。

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

              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;
                  }
              }
              

              【讨论】:

                【解决方案9】:

                此时,iterable 被消费了。

                这意味着它的内部 [[GeneratorState]] 是completed

                有没有一种方法可以只通过不重新调用 generator() 将可迭代移回起点

                没有。规范说明

                一旦生成器进入“完成”状态,它就永远不会离开它,并且其关联的执行上下文也永远不会恢复。此时可以丢弃与生成器相关的任何执行状态。

                或者可能通过重新调用生成器(),仅通过使用原型 或可迭代对象中可用的构造方法

                没有。虽然规范中没有明确说明,但iterable object 上没有比 [[GeneratorState]] 和 [[GeneratorContext]] 更多的特定于实例的属性。

                但是,信息丰富的"Generator Object Relationships" grapic 声明:

                每个生成器函数都有一个没有构造函数属性的关联原型。因此,生成器实例不会公开对其生成器函数的访问。

                我希望能够将可迭代对象传递给其他范围

                改为传递生成器函数。或者产生新生成器实例的东西。

                【讨论】:

                • 我认为考虑到当前的规范,这是有道理的。感谢您的报价和建议。
                【解决方案10】:

                据我所知,这是不可能的。根据生成器上的this useful wikidraft version of ES6,一旦你从它返回(而不是屈服),它就会将其置于"closed" 状态并且无法将其移回"newborn" 状态是新生成器的启动方式。

                您可能必须将回调传递给其他范围以创建新生成器。作为一种变通方法,如果需要,您甚至可以将该回调作为自定义方法添加到您发送到另一个范围的生成器上,并且该回调将为另一个范围创建一个新的生成器。

                如果您考虑生成器的工作原理,它们必须从头开始执行以重置其初始状态,而且根本没有理由支持这一点。这类似于问为什么你不能在现有对象上重新执行构造函数并期望在同一个对象中有一个处女对象。虽然这在技术上都是可行的,但要正确地工作很麻烦,而且真的没有理由支持它。如果您想要一个处女对象,只需创建一个新对象。与生成器相同。


                这有点小题大做,但值得深思。您可以制作一个重复自身的生成器。假设你的生成器是这样工作的:

                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);
                }
                

                您将有一个无限循环,因此必须非常小心地使用它。仅供参考,上面的 wiki 显示了无限 Fibonacci 序列的示例,因此当然可以考虑使用无限生成器。

                【讨论】:

                • 这是一个有趣的想法。不过,我不相信它会在我的情况下起作用,因为我无法修改原始生成器(我正在考虑扩展我的一个包含数组操作的库,以便也与生成器操作一起使用)。跨度>
                • @dvlsg - 你可以用你自己的迭代器包装一个迭代器,它有一个.restart() 方法或类似的东西。您的 .restart() 方法只会从您的库中创建一个新的迭代器,并且在您的外部迭代器上对 .next() 的下一次调用将使用新的内部迭代器重新开始。这里有很多可能性可以让远程范围重新开始。
                • 我想这就是我要走的路。希望。我遇到的部分问题是能够将方法从一个生成器链接到下一个生成器——类似于 C# Linq,您可以在其中调用 MyEnumerable.Where(x =&gt; x.id &gt; 3).Select(x =&gt; x.value); 但是!这是另一天的问题。
                【解决方案11】:

                根据draft version of ES6

                一旦生成器进入"completed" 状态,它就永远不会离开它,并且其关联的执行上下文永远不会恢复。此时可以丢弃与生成器相关的任何执行状态。

                因此,一旦完成,就无法重置它。这样做也是有道理的。我们称它为生成器是有原因的:)

                【讨论】:

                • 无赖。我想我在咆哮 C# 的树(这是我习惯的),你可以在同一个 IEnumerable 上执行 foreach 循环,而无需重新初始化它。我想这是在内部调用 GetEnumerator() 本质上是在重新创建迭代器。
                • 假设指针位于六项中的第四项。它还没有完成对吧..?我想那有可能吗? ;)
                猜你喜欢
                • 1970-01-01
                • 2020-08-31
                • 1970-01-01
                • 2016-07-10
                • 1970-01-01
                • 2021-01-15
                • 1970-01-01
                • 2021-09-05
                • 1970-01-01
                相关资源
                最近更新 更多