【问题标题】:How can I find out if a javascript iterator terminates early?如何确定 javascript 迭代器是否提前终止?
【发布时间】:2018-03-06 23:12:04
【问题描述】:

假设我有一个生成器:

function* source() {
  yield "hello"; yield "world";
}

我创建了可迭代对象,使用 for 循环进行迭代,然后在迭代器完全完成之前跳出循环(返回完成)。

function run() {
  for (let item of source()) {
    console.log(item);
    break;
  }
}

问题:我怎样才能从可迭代方面发现迭代器提前终止了?

如果您尝试直接在生成器本身中执行此操作,似乎没有任何反馈:

function* source2() {
  try {
    let result = yield "hello";
    console.log("foo");
  } catch (err) {
    console.log("bar");
  }
}

... "foo" 和 "bar" 都没有被记录。

【问题讨论】:

  • 得知有办法做到这一点我会感到非常惊讶。
  • 如果将console.log('foo')移到yield 'hello'上方会有什么不同吗?
  • @Pointy 找到了方法。 :D

标签: javascript typescript iterator generator async-iterator


【解决方案1】:

我注意到 typescript 将Iterator (lib.es2015) 定义为:

interface Iterator<T> {
  next(value?: any): IteratorResult<T>;
  return?(value?: any): IteratorResult<T>;
  throw?(e?: any): IteratorResult<T>;
} 

我截获了这些方法并记录了调用,看起来如果一个迭代器提前终止——至少通过for-loop——然后调用return方法。 如果消费者抛出错误,它也会被调用。如果允许循环完全迭代迭代器return 被调用。

Return黑客

所以,我做了一些小技巧来允许捕获 another 可迭代的 - 所以我不必重新实现迭代器。

function terminated(iterable, cb) {
  return {
    [Symbol.iterator]() {
      const it = iterable[Symbol.iterator]();
      it.return = function (value) {
        cb(value);
        return { done: true, value: undefined };
      }
      return it;
    }
  }
}

function* source() {
  yield "hello"; yield "world";
}

function source2(){
  return terminated(source(), () => { console.log("foo") });
}


for (let item of source2()) {
  console.log(item);
  break;
}

它有效!

你好
呵呵

删除break 你会得到:

你好
世界

检查每个yield

在输入这个答案时,我意识到更好的问题/解决方案是在原始生成器方法中找出

我能看到将信息传递回原始迭代的唯一方法是使用next(value)。因此,如果我们选择一些唯一值(例如 Symbol.for("terminated"))来表示终止,然后我们将上述 return-hack 更改为调用 it.next(Symbol.for("terminated"))

function* source() {
  let terminated = yield "hello";
  if (terminated == Symbol.for("terminated")) {
    console.log("FooBar!");
    return;
  }
  yield "world";
}

function terminator(iterable) {
  return {
    [Symbol.iterator]() {
      const it = iterable[Symbol.iterator]();
      const $return = it.return;
      it.return = function (value) {
        it.next(Symbol.for("terminated"));
        return $return.call(it)
      }
      return it;
    }
  }
}

for (let item of terminator(source())) {
  console.log(item);
  break;
}

成功了!

你好
FooBar!

链式级联 Return

如果你链接一些额外的转换迭代器,那么 return 调用会级联它们:

function* chain(source) {
  for (let item of source) { yield item; }
}

for (let item of chain(chain(terminator(source())))) {
  console.log(item);
  break
}

你好
FooBar!

我已经包装了上述解决方案as a package。它同时支持[Symbol.iterator][Symbol.asyncIterator]。我对异步迭代器的情况特别感兴趣,尤其是在需要正确处理某些资源时。

【讨论】:

    【解决方案2】:

    有一种更简单的方法可以做到这一点:使用 finally 块。

    function *source() {
      let i;
    
      try {
        for(i = 0; i < 5; i++)
          yield i;
      }
      finally {
        if(i !== 5)
          console.log('  terminated early');
      }
    }
    
    console.log('First:')
    
    for(const val of source()) {
      console.log(`  ${val}`);
    }
    
    console.log('Second:')
    
    for(const val of source()) {
      console.log(`  ${val}`);
    
      if(val > 2)
        break;
    }
    

    ...产量:

    First:
      0
      1
      2
      3
      4
    Second:
      0
      1
      2
      3
      terminated early
    

    【讨论】:

    • 非常感谢您。我正在编写一个重迭代器的库,完全忘记了finally 使生成器上的每一个合约都变得疯狂。
    【解决方案3】:

    我也遇到过类似的需要来确定迭代器何时提前终止。公认的答案非常聪明,可能是一般解决问题的最佳方法,但我认为此解决方案也可能对其他用例有所帮助。

    比如说,你有一个无限的可迭代对象,比如MDN's Iterators and Generators docs 中描述的斐波那契数列。

    在任何类型的循环中,都需要设置一个条件以尽早跳出循环,就像已经给出的解决方案一样。但是,如果您想解构可迭代对象以创建值数组怎么办?在这种情况下,您需要限制迭代次数,本质上是在可迭代对象上设置最大长度。

    为此,我编写了一个名为limitIterable 的函数,该函数将一个可迭代对象、一个迭代限制和一个在迭代器提前终止时执行的可选回调函数作为参数。返回值是使用 Immediately Invoked (Generator) Function Expression 创建的 Generator 对象(它既是迭代器又是可迭代对象)。

    当生成器执行时,无论是在 for..of 循环中、解构还是调用 next() 方法,它都会检查是 iterator.next().done === true 还是 iterationCount &lt; iterationLimit。在像斐波那契数列这样的无限可迭代的情况下,后者总是会导致 while 循环退出。但是,请注意,也可以设置一个大于某些有限可迭代的长度的迭代限制,并且一切仍然有效。

    在任何一种情况下,一旦退出 while 循环,将检查最近的结果以查看迭代器是否完成。如果是这样,将使用原始迭代的返回值。如果不是,则执行可选的回调函数并用作返回值。

    请注意,此代码还允许用户将值传递给next(),然后将其传递给原始可迭代对象(请参阅附加代码 sn-p 中使用 MDN 的斐波那契序列的示例)。它还允许在回调函数中设置的迭代限制之外对next() 进行额外调用。

    运行代码 sn -p 来查看几个可能的用例的结果!这是limitIterable 本身的功能代码:

    function limitIterable(iterable, iterationLimit, callback = (itCount, result, it) => undefined) {
       // callback will be executed if iterator terminates early
       if (!(Symbol.iterator in Object(iterable))) {
          throw new Error('First argument must be iterable');
       }
       if (iterationLimit < 1 || !Number.isInteger(iterationLimit)) {
          throw new Error('Second argument must be an integer greater than or equal to 1');
       }
       if (!(callback instanceof Function)) {
          throw new Error('Third argument must be a function');
       }
       return (function* () {
          const iterator = iterable[Symbol.iterator]();
          // value passed to the first invocation of next() is always ignored, so no need to pass argument to next() outside of while loop
          let result = iterator.next();
          let iterationCount = 0;
          while (!result.done && iterationCount < iterationLimit) {
             const nextArg = yield result.value;
             result = iterator.next(nextArg);
             iterationCount++;
          }
          if (result.done) {
             // iterator has been fully consumed, so result.value will be the iterator's return value (the value present alongside done: true)
             return result.value;
          } else {
             // iteration was terminated before completion (note that iterator will still accept calls to next() inside the callback function)
             return callback(iterationCount, result, iterator);
          }
       })();
    }
    

    function limitIterable(iterable, iterationLimit, callback = (itCount, result, it) => undefined) {
       // callback will be executed if iterator terminates early
       if (!(Symbol.iterator in Object(iterable))) {
          throw new Error('First argument must be iterable');
       }
       if (iterationLimit < 1 || !Number.isInteger(iterationLimit)) {
          throw new Error('Second argument must be an integer greater than or equal to 1');
       }
       if (!(callback instanceof Function)) {
          throw new Error('Third argument must be a function');
       }
       return (function* () {
          const iterator = iterable[Symbol.iterator]();
          // value passed to the first invocation of next() is always ignored, so no need to pass argument to next() outside of while loop
          let result = iterator.next();
          let iterationCount = 0;
          while (!result.done && iterationCount < iterationLimit) {
             const nextArg = yield result.value;
             result = iterator.next(nextArg);
             iterationCount++;
          }
          if (result.done) {
             // iterator has been fully consumed, so result.value will be the iterator's return value (the value present alongside done: true)
             return result.value;
          } else {
             // iteration was terminated before completion (note that iterator will still accept calls to next() inside the callback function)
             return callback(iterationCount, result, iterator);
          }
       })();
    }
    
    // EXAMPLE USAGE //
    // fibonacci function from:
    //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators#Advanced_generators
    function* fibonacci() {
       let fn1 = 0;
       let fn2 = 1;
       while (true) {
          let current = fn1;
          fn1 = fn2;
          fn2 = current + fn1;
          let reset = yield current;
          if (reset) {
             fn1 = 0;
             fn2 = 1;
          }
       }
    }
    
    console.log('String iterable with 26 characters terminated early after 10 iterations, destructured into an array. Callback reached.');
    const itString = limitIterable('abcdefghijklmnopqrstuvwxyz', 10, () => console.log('callback: string terminated early'));
    console.log([...itString]);
    console.log('Array iterable with length 3 terminates before limit of 4 is reached. Callback not reached.');
    const itArray = limitIterable([1,2,3], 4, () => console.log('callback: array terminated early?'));
    for (const val of itArray) {
       console.log(val);
    }
    
    const fib = fibonacci();
    const fibLimited = limitIterable(fibonacci(), 9, (itCount) => console.warn(`Iteration terminated early at fibLimited. ${itCount} iterations completed.`));
    console.log('Fibonacci sequences are equivalent up to 9 iterations, as shown in MDN docs linked above.');
    console.log('Limited fibonacci: 11 calls to next() but limited to 9 iterations; reset on 8th call')
    console.log(fibLimited.next().value);
    console.log(fibLimited.next().value);
    console.log(fibLimited.next().value);
    console.log(fibLimited.next().value);
    console.log(fibLimited.next().value);
    console.log(fibLimited.next().value);
    console.log(fibLimited.next().value);
    console.log(fibLimited.next(true).value);
    console.log(fibLimited.next().value);
    console.log(fibLimited.next().value);
    console.log(fibLimited.next().value);
    console.log('Original (infinite) fibonacci: 11 calls to next(); reset on 8th call')
    console.log(fib.next().value);
    console.log(fib.next().value);
    console.log(fib.next().value);
    console.log(fib.next().value);
    console.log(fib.next().value);
    console.log(fib.next().value);
    console.log(fib.next().value);
    console.log(fib.next(true).value);
    console.log(fib.next().value);
    console.log(fib.next().value);
    console.log(fib.next().value);

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-08-09
      • 1970-01-01
      • 2020-09-04
      • 2013-08-22
      • 1970-01-01
      • 2010-09-22
      • 1970-01-01
      • 2018-09-10
      相关资源
      最近更新 更多