for..of 和迭代器状态

for..of and the iterator state

考虑这个 python 代码

it = iter([1, 2, 3, 4, 5])

for x in it:
    print x
    if x == 3:
        break

print '---'

for x in it:
    print x

它打印 1 2 3 --- 4 5,因为迭代器 it 记住它在循环中的状态。当我在 JS 中做看似相同的事情时,我得到的只是 1 2 3 ---

function* iter(a) {
    yield* a;
}

it = iter([1, 2, 3, 4, 5])

for (let x of it) {
    console.log(x)
    if (x === 3)
        break
}

console.log('---')

for (let x of it) {
    console.log(x)
}

我错过了什么?

不幸的是,JS 中的生成器对象不可重用。 在 MDN

上明确说明

Generators should not be re-used, even if the for...of loop is terminated early, for example via the break keyword. Upon exiting a loop, the generator is closed and trying to iterate over it again does not yield any further results.

如前所述,发电机是一次性的。

但是通过将数组包装在闭包中并返回一个新的生成器来模拟 re-usable 迭代器很容易..

例如

function resume_iter(src) {
  const it = src[Symbol.iterator]();
  return {
    iter: function* iter() {
      while(true) {
        const next = it.next();
        if (next.done) break;
        yield next.value;
      }
    }
  }
}

const it = resume_iter([1,2,3,4,5]);

for (let x of it.iter()) {
    console.log(x)
    if (x === 3)
        break
}

console.log('---')

for (let x of it.iter()) {
    console.log(x)
}



console.log("");
console.log("How about travesing the DOM");

const it2 = resume_iter(document.querySelectorAll("*"));

for (const x of it2.iter()) {
  console.log(x.tagName);
  //stop at first Script tag.
  if (x.tagName === "SCRIPT") break;
}

console.log("===");

for (const x of it2.iter()) {
  console.log(x.tagName);
}

除了 Andrey 的回答之外,如果您想拥有与 Python 脚本中相同的功能,因为在退出循环时生成器无法 re-used,您可以 re-create 每次循环之前的迭代器,并跟踪循环结束的位置以排除对 already-processed 结果的处理,如下所示:

function* iter(a) {
  yield* a;
}

var broken = 0;

iterate();
console.log('---');
iterate();

function iterate() {
  var it = iter([1, 2, 3, 4, 5]);
  for (let x of it) {
    if (x <= broken)
      continue;
    console.log(x);
    if (x === 3) {
      broken = x;
      break;
    }
  }
}

这更多地与 for..of 的操作方式有关,而不是迭代器的可重用性。如果您要手动拉取迭代器的下一个值,您可以根据需要多次调用它,它会从之前的状态恢复。

这使得这样的事情成为可能:

function* iter(a) {
  yield* a;
}

let values = [1, 2, 3, 4, 5];
let it = iter(values)

for (let i = 0, n = values.length; i < n; i++) {
  let x = it.next().value
  console.log(x)
  if (x === 3)
    break
}

console.log('---')

for (let x of it) {
  console.log(x)
}

对于不依赖于 values 数组的 while 循环也可以这样做:

function* iter(a) {
  yield* a;
}

let it = iter([1, 2, 3, 4, 5]),
  contin = true

while (contin && (x = it.next().value)) {
  console.log(x)
  if (x === 3)
    contin = false
}

console.log('---')

for (let x of it) {
  console.log(x)
}

第二个示例(while 循环)略有不同,因为在条件评估期间分配了 x。它假定 x 的所有值都是真实的,因此 undefined 可以用作终止条件。如果不是这种情况,则需要在循环块中分配它并且必须设置终止条件。类似于 if(x===undefined)contin=false 或检查迭代器是否已到达其输入的末尾。

正如其他答案所指出的,for..of 在任何情况下都会关闭迭代器,因此需要另一个包装器来保存状态,例如

function iter(a) {
    let gen = function* () {
        yield* a;
    }();

    return {
        next() {
            return gen.next()
        },
        [Symbol.iterator]() {
            return this
        }
    }
}


it = iter([1, 2, 3, 4, 5]);

for (let x of it) {
    console.log(x);
    if (x === 3)
        break;
}

console.log('---');

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

根据规范,此行为是预期的,但有一个简单的解决方案。 for..of循环在循环结束后调用return method

Invoking this method notifies the Iterator object that the caller does not intend to make any more next method calls to the Iterator.

解决方案

你当然可以用一个自定义的函数替换那个函数,它不会关闭实际的迭代器,就在循环中使用它之前:

iter.return = value => ({ value, done: true });

示例:

function* iter(a) {
    yield* a;
}

it = iter([1, 2, 3, 4, 5])
it.return = () => ({})

for (let x of it) {
    console.log(x)
    if (x === 3)
        break
}

console.log('---')

for (let x of it) {
    console.log(x)
}