JavaScript向迭代器添加“ return”方法无法正确关闭迭代器

问题描述

我正在学习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方法,因为我获得了控制台日志,但是即使我在{{1}中返回了{ done: true },实际上也不会终止迭代器}方法

解决方法

您的示例可以简化为

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返回一次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()直到返回结果为done: true,并在某些情况下调用.return。迭代器本身的实现取决于确保迭代器本身正确进入“关闭”状态。

所有这些都可以通过使用生成器函数来简化,因为生成器对象具有自动包含的内部“关闭”状态,作为已返回的函数的副作用,例如

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方法在所有后续调用中也返回{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]方法来多次返回相同的对象,而最简单的方法是让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: 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!
}