JavaScript 向迭代器添加 "return" 方法未正确关闭迭代器

JavaScript adding "return" method to an iterator doesn't properly close the iterator

我正在学习 JavaScript ES6 迭代器模式并遇到了这个问题:

const counter = [1, 2, 3, 4, 5];
const iter = counter[Symbol.iterator]();
iter.return = function() {
  console.log("exiting early");
  return { done: true };
};

for (let i of iter) {
  console.log(i);
  if (i >= 3) {
    break;
  }
}
console.log('---');
for (let i of iter) {
  console.log(i);
}

// 1
// 2
// 3
// exiting early
// ---
// 4
// 5

所以我向从数组中提取的迭代器添加了一个 return 方法定义。虽然调用了 return 方法,但它实际上并没有关闭迭代器。相比之下,如果我在定义中定义迭代器 return 方法,它将按预期工作:

class Counter {
  [Symbol.iterator]() {
    let count = 1;
    return {
      next() {
        if (count <= 5) {
          return {
            done: false,
            value: count++
          }
        } else {
          return {
            done: true,
            value: undefined
          }
        }
      },
      return() {
        console.log('exiting early');
        return { done: true, value: undefined };
      }
    }
  }
}

const myCounter = new Counter();
iter = myCounter[Symbol.iterator]();
for (let i of myCounter) {
  console.log(i);
  if (i >= 3) {
    break;
  }
}
console.log('---');
for (let i of myCounter) {
  console.log(i);
}

// 1
// 2
// 3
// exiting early
// ---
// 1
// 2
// 3
// 4
// 5

我的问题是,为什么会出现这种意外行为?我假设如果 return 方法没有被调用,那么迭代器将不会关闭,直到它通过调用 next 到达最后。但是添加 return 属性 将正确地“调用” return 方法,因为我得到了控制台日志,但实际上并没有终止迭代器,即使我 returned { done: true }return 方法中。

你的例子可以简化为

let count = 1;
const iter = {
  [Symbol.iterator]() { return this; },
  next() {
    if (count <= 5) {
      return {
        done: false,
        value: count++
      }
    } else {
      return {
        done: true,
        value: undefined
      }
    }
  },
  return() {
    console.log('exiting early');
    return { done: true, value: undefined };
  }
};

for (let i of iter) {
  console.log(i);
  if (i >= 3) {
    break;
  }
}
console.log('---');
for (let i of iter) {
  console.log(i);
}

所以iter只是一个普通对象。您将它传递给 for..of 循环两次。

您对迭代器接口的工作方式做出了错误的假设。核心问题是此代码中没有任何内容可以存储和跟踪 iter 已 returned done: true 一次的事实,因此应该继续这样做。如果那是你想要的行为,你需要自己做,例如

let count = 1;
let done = false;
const iter = {
  [Symbol.iterator]() { return this; },
  next() {
    if (!done && count <= 5) {
      return {
        value: count++
      }
    } else {
      done = true;
      return { done };
    }
  },
  return() {
    done = true;
    console.log('exiting early');
    return { done };
  }
};

for..of 循环本质上是调用 .next() 直到 return 结果为 done: true,并在某些情况下调用 .return。由迭代器本身的实现来确保它正确地进入“关闭”状态。

所有这些也可以通过使用生成器函数来简化,因为生成器对象具有内部“关闭”状态,自动包含为具有 returned 的函数的 side-effect,例如

function* counter() {
  let counter = 1;
  while (counter <= 5) yield counter++;
}

const iter = counter();
for (let i of iter) {
  console.log(i);
  if (i >= 3) {
    break;
  }
}
console.log('---');
for (let i of iter) {
  console.log(i);
}

你的两个 return 方法都没有真正关闭迭代器。为此,他们需要记录迭代器的新状态,并由此导致 next 方法在所有后续调用中也 return {done: true} - 这就是“关闭”的实际含义.

我们可以在生成器中看到这种行为:

const iter = function*(){ yield 1; yield 2; yield 3; }();
console.log(iter.next());
console.log(iter.return());
console.log(iter.next());

您的第一个代码段存在问题,您已覆盖 iter.return,您的方法被调用(从日志中可以看出)但它从未真正关闭 iter。潜在的问题是数组迭代器 无法关闭 ,它们通常根本没有 return 方法。您还必须覆盖 iter.next 方法来模拟此方法。

你的第二个片段有问题,它实际上并没有尝试迭代 iter,而是迭代了 myCounter 两次,这为每个循环创建了一个新的迭代器对象。相反,我们需要使用 [Symbol.iterator] 方法多次 return 同一个对象,最简单的方法是让 Counter 实现迭代器接口本身。我们现在可以重现意外行为:

class Counter {
  count = 1;
  [Symbol.iterator]() {
    return this;
  }
  next() {
    if (this.count <= 5) {
      return {done: false, value: this.count++ };
    } else {
      return {done: true, value: undefined};
    }
  }
  return() {
    console.log('exiting early');
    return { done: true, value: undefined };
  }
}

const iter = new Counter();
for (let i of iter) {
  console.log(i);
  if (i >= 3) {
    break;
  }
}
console.log('---');
for (let i of iter) {
  console.log(i);
}

为了修复该行为,我们将通过 return 方法将计数设置为超过 5 来关闭迭代器:

class Counter {
  count = 1;
  [Symbol.iterator]() {
    return this;
  }
  next() {
    if (this.count <= 5) {
      return {done: false, value: this.count++ };
    } else {
      return {done: true, value: undefined};
    }
  }
  return() {
    this.count = 6;
//  ^^^^^^^^^^^^^^^
    console.log('exiting early');
    return { done: true, value: undefined };
  }
}

const iter = new Counter();
for (let i of iter) {
  console.log(i);
  if (i >= 3) {
    break;
  }
}
console.log('---');
for (let i of iter) {
  console.log(i); // not executed!
}