for..of 和迭代器状态

9
考虑以下 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)
}

我错过了什么?


1
您有一个生成器,只能使用一次。 - epascarello
6个回答

6

很不幸,在JS中,生成器对象是不可重用的。 在MDN上明确说明了这一点。

即使通过break关键字提前终止for...of循环,也不应该重新使用生成器。退出循环后,生成器被关闭,并且尝试再次迭代它将不产生任何结果。


是的,这(不幸地)似乎就是答案。ECMA标准链接http://www.ecma-international.org/ecma-262/7.0/#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset,项目k。 - georg

3
这种行为是符合规范的,但有一个简单的解决方案。在for..of循环结束后,会调用return方法

调用此方法会通知迭代器对象,调用者不打算再进行任何next方法调用。

解决方案

你当然可以使用自定义函数来替换这个函数,在循环之前使用它,以避免关闭实际的迭代器:
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)
}


太好了!我不知道return - georg

3

如前所述,生成器只能使用一次。

但是,将数组包装在闭包中并返回新的生成器就可以轻松模拟可重用迭代器。

例如:

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


不错,但我希望迭代器不知道底层类型,因为它不一定是数组。 - georg
@georg 哦,那么如何在闭包中包装一个可迭代对象呢?更新代码片段以处理任何可迭代对象。 - Keith
@georg 更新了代码片段以遍历DOM节点,因为那不是一个数组,基本上在第一个SCRIPT标签处停止,然后再次恢复。 - Keith

2
这更多地涉及到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或检查迭代器是否已经到达其输入的末尾来实现。

好主意,我已经发布了一个包装器,它可以手动从迭代器中提取值,从而保留其状态。 - georg

0
除了Andrey的回答之外,如果您想要与Python脚本中相同的功能,由于生成器在退出循环时无法重复使用,您可以在每次循环之前重新创建迭代器,并跟踪循环结束被打破的位置,以排除已处理结果的处理,如下所示:

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


1
你仍然在循环两次。 - Nina Scholz

-1
正如其他答案所指出的那样,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);
}


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