问题描述
我正在学习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!
}