好的,我想我会总结一下我们在 cmets 中学到的一些东西,并添加更多内容,然后写下您具体问题的答案。
[...x] 语法
[...x] 语法适用于支持iterables 接口的事物。而且,要支持可迭代接口,您所要做的就是支持 Symbol.iterator 属性以提供一个(在调用时)返回迭代器的函数。
内置迭代器也是可迭代的
Javascript 中内置的所有迭代器都派生自同一个 IteratorPrototype。迭代器不需要这样做,这是内置迭代器做出的选择。
这个内置的IteratorPrototype 也是一个Iterable。它支持Symbol.iterator 属性,这是一个只执行return this 的函数。这是by specification。
这意味着所有内置迭代器(例如 someSet.values())都将使用 [...x] 语法。我不确定为什么这非常有用,但它肯定会导致混淆 Iterable 可以做什么以及 Iterator 可以做什么,因为这些内置的迭代器可以起到任何作用。
这会导致一些奇怪的行为,因为如果你这样做:
let s = new Set([1,2,3]);
let iter = s.values(); // gets an iterator
let x = [...iter];
let y = [...iter];
console.log(x);
console.log(y);
第二个[...iter] 是一个空数组,因为这里只有一个迭代器。事实上,x === y。因此第一个let x = [...iter]; 耗尽了迭代器。它位于done 上,无法再次迭代集合。这是因为内置迭代器的这种时髦行为,它们表现为可迭代的,但只是return this。他们不会创建一个新的迭代器,它可以像您使用实际的可迭代集合时那样再次迭代集合。每次访问s[Symbol.iterator]() 时,这个集合迭代器都会返回一个全新的迭代器,如下所示:
let s = new Set([1,2,3]);
let x = [...s];
let y = [...s];
console.log(x);
console.log(y);
普通迭代器不适用于[...x]
要成为一个迭代器,您需要实现的只是支持.next() 方法并使用适当的对象进行响应。其实这里有一个超级简单的迭代器,符合规范:
const iter = {
i: 1,
next: function() {
if (this.i <= 3) {
return { value: this.i++, done: false };
} else {
return { value: undefined, done: true };
}
}
}
如果你尝试做let x = [...iter];,它会抛出这个错误:
TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
但是,如果您通过向其添加适当的 [Symbol.iterator] 属性使其成为 Iterable,它将作为 [...iter] 工作;
const iter = {
i: 1,
next: function() {
if (this.i <= 3) {
return { value: this.i++, done: false };
} else {
return { value: undefined, done: true };
}
},
[Symbol.iterator]: function() { return this; }
}
let x = [...iter];
console.log(x);
然后,它可以作为[...iter] 工作,因为它现在也是一个可迭代对象。
发电机
Generator 函数在被调用时会返回一个 Generator 对象。根据spec,该生成器对象的行为既是Iterator,又是Iterable。故意无法判断此 Iterator/Iterable 是否来自生成器,这显然是 done on purpose。调用代码只知道它是Iterator/Iterable,而生成器函数只是创建对调用代码透明的序列的一种方法。它像任何其他迭代器一样被迭代。
两个迭代器的故事
在您最初的问题中,您展示了两个迭代器,一个重复工作,一个不重复工作。这里有两件事在起作用。
首先,一些迭代器“消耗”了它们的序列,并且没有办法重复迭代相同的序列。这些将是制造的序列,而不是静态集合。
其次,在您的第一个代码示例中:
const iterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 3;
yield 5;
}
}
console.log([...iterable]);
console.log([...iterable]);
console.log([...iterable]);
单独的迭代器
那个iterable是一个iterable。它不是一个迭代器。你可以通过调用iterable[Symbol.iterator]() 来请求它提供一个迭代器,这就是[...iterable] 所做的。但是,当你这样做时,它会返回一个全新的 Generator 对象,它是一个全新的迭代器。每次调用 iterable[Symbol.iterator]() 或使用 [...iterable] 调用时,都会得到一个新的不同的迭代器。
你可以在这里看到:
const iterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 3;
yield 5;
}
}
let iterA = iterable[Symbol.iterator]();
let iterB = iterable[Symbol.iterator]();
// shows false, separate iterators on separate generator objects
console.log(iterA === iterB);
因此,您正在为每个迭代器创建一个全新的序列。它重新调用生成器函数以获取新的生成器对象。
相同的迭代器
但是,用你的第二个例子:
function* generatorFn() {
yield 1;
yield 3;
yield 5;
}
const iterable = generatorFn();
console.log([...iterable]);
console.log([...iterable]);
console.log([...iterable]);
不一样。你在这里称呼iterable 是我喜欢的pseudo-iterable。它同时实现了Iterable 和Iterator 接口,但是当您像[...iterable] 那样要求它提供Iterator 时,它每次(本身)都返回相同的对象。因此,每次您执行[...iterable] 时,它都在同一个迭代器上运行。但是在您第一次执行[...iterable] 之后,该迭代器已用尽并且处于done 状态。所以,后两个[...iterable] 是空数组。迭代器没有什么可提供的了。
您的问题
是否有一个可迭代对象是否应该重复迭代的规则?
不是真的。首先,最终到达done 状态的给定迭代器(非无限迭代器)一旦到达done 状态就会给出任何结果。根据迭代器的定义。
因此,代表某种静态序列的Iterable 是否可以重复迭代取决于它在每次被询问迭代器时提供的Iterator 是否是新的和唯一的,并且我们'在上面的两个示例中已经看到,Iterable 可以采用任何一种方式。
它每次都可以产生一个新的、唯一的迭代器,每次都通过序列呈现一个新的迭代。
或者,Iterable 每次都可以生成完全相同的Iterator。如果这样做,一旦该迭代器进入done 状态,它就会卡在那里。
还请记住,某些 Iterables 表示可能不可重复的动态集合/序列。对于 Set 或 Map 之类的东西,情况并非如此,但是更多自定义类型的 Iterables 可能在迭代时基本上“消耗”他们的集合,当它完成时,即使你得到一个新的新鲜迭代器。
想象一个迭代器,它给你一个价值在 1 美元到 10 美元之间的随机金额的代码,并在你每次向迭代器询问下一个值时从你的银行余额中减去它。在某些时候,您的银行余额达到$0 并且该迭代器已完成,即使获得一个新的迭代器仍然必须处理相同的$0 银行余额(没有更多值)。这将是一个迭代器的例子
“消耗”值或某些资源,只是不可重复。
但我想知道 iterable 作为一种对象类型,是否有明确定义的行为是否应该或不应该重复迭代。
没有。它是特定于实现的,完全取决于您正在迭代的内容。使用像 Set 或 Map 或 Array 这样的静态集合,您可以获取一个新的迭代器并每次生成一个新的迭代。但是,我称之为psuedo-iterable(每次请求时返回相同迭代器的迭代)或迭代时序列被“消耗”的迭代可能无法重复迭代。所以,它可以故意是任何一种方式。没有标准的方法。这取决于正在迭代的内容。
测试你所拥有的
这里有一些有用的测试可以帮助人们理解一些事情:
// could do a more comprehensive test by calling `obj.next()` to see if
// it returns an appropriate object with appropriate properties, but
// that is destructive to the iterator (consumes that value)
// so we keep this one non-destructive
function isLikeAnIterator(obj) {
return typeof obj === "object" && typeof obj.next === "function)";
}
function isIterable(obj) {
if (typeof obj === "object" && typeof obj[Symbol.iterator] === "function") {
let iter = obj[Symbol.iterator]();
return isLikeAnIterator(iter);
}
return false;
}
// A pseudo-iterable returns the same iterator each time
// Sometimes, the pseudo-iterable returns itself as the iterator too
function isPseudoIterable(obj) {
if (isIterable(obj) {
let iterA = obj[Symbol.iterator]();
if (iterA === this) {
return true;
}
let iterB = obj[Symbol.iterator]();
return iterA === iterB;
}
return false;
}
function isGeneratorObject(obj) {
if (!isIterable(obj) !! !isLikeAnIterator(obj) {
// does not meet the requirements of a generator object
// which must be both an iterable and an iterator
return false;
}
throw new Error("Can't tell if it's a generator object or not by design");
}