如何判断JavaScript迭代器是否提前终止?

3
假设我有一个生成器:
function* source() {
  yield "hello"; yield "world";
}

我创建了一个可迭代对象,在for循环中进行迭代,但在迭代器完全完成(返回done)之前就退出了循环。

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" 都没有被记录。


我会非常惊讶地发现有一种方法可以做到这一点。 - Pointy
如果将 console.log('foo') 移动到 yield 'hello' 上面,会有什么不同吗? - mikkelrd
@Pointy 找到了一种方法。 :D - Meirion Hughes
3个回答

7

编辑:请查看新的已接受答案。我将保留这个答案,因为它是有效的/曾经有效,而且当我能够破解一个解决方案时,我感到非常高兴。然而,正如您在已接受的答案中所看到的那样,最终的解决方案现在变得如此简单,因为它已被确认。

我注意到 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 hack

所以,我进行了一些hack,以允许捕获另一个可迭代对象 - 这样我就不必重新实现迭代器。
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;
}

而且它有效!

你好
foo

删除 break 你将得到:

你好
世界

每次 yield 后检查

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

我唯一能想到向原始可迭代对象传递信息的方法是使用 next(value)。因此,如果我们选择一些独特的值(比如 Symbol.for("terminated"))来表示终止,并且修改上面的返回窍门以调用 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 调用将级联通过它们:

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

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

你好
FooBar!

我将上面的解决方案封装成了一个包。它支持[Symbol.iterator][Symbol.asyncIterator]两种迭代器。特别是在一些资源需要正确释放的情况下,异步迭代器非常有意义。


4

有一种更简单的方式来完成这个任务:使用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会使生成器上的每个合同都变得疯狂。 - polkovnikov.ph

0

我遇到了类似的需求,需要找出迭代器何时提前终止。接受的答案非常聪明,可能是通用解决方案中最好的方法,但我认为这个解决方案也可以对其他用例有所帮助。

例如,假设您有一个无限可迭代对象,例如在MDN的迭代器和生成器文档中描述的斐波那契数列。

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

为此,我编写了一个名为limitIterable的函数,它将可迭代对象、迭代限制和可选回调函数作为参数。返回值是使用立即调用(生成器)函数表达式创建的生成器对象(既是迭代器又是可迭代对象)。

当生成器被执行时,无论是在for..of循环中、使用解构或调用next()方法,它都会检查iterator.next().done === trueiterationCount < iterationLimit。对于像斐波那契数列这样的无限可迭代对象,后者总是会导致while循环退出。但是,请注意,您也可以设置一个大于某些有限可迭代对象长度的iterationLimit,并且一切仍将正常工作。

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

请注意,此代码还允许用户传递值给next(),这些值将依次传递给原始可迭代对象(请参见附加的代码片段中使用MDN的斐波那契数列的示例)。它还允许在回调函数中超出设置的iterationLimit进行额外的next()调用。

运行代码片段以查看几种可能用例的结果!这里是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);


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