在 JavaScript 中,可迭代对象应该可重复迭代吗?

In JavaScript, should an iterable be repeatedly iterable?

提问人:nonopolarity 提问时间:1/5/2020 最后编辑:nonopolarity 更新时间:6/27/2021 访问量:303

问:

我发现有些可迭代可以重复迭代:

const iterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 3;
    yield 5;
  }
}

console.log([...iterable]);
console.log([...iterable]);
console.log([...iterable]);

虽然有些人不能:

function* generatorFn() {
  yield 1;
  yield 3;
  yield 5;
}

const iterable = generatorFn();

console.log([...iterable]);
console.log([...iterable]);
console.log([...iterable]);

是否有规则可以迭代,是否应该重复迭代?

我理解为什么它们的行为不同(这是因为第二种情况,当调用函数时,会返回相同的迭代器(这是它本身。可以尝试,它会回来. 也是一个函数。所以在这种情况下,是一个生成器对象,一个可迭代对象,一个迭代器,这三个)。但是我想知道可迭代是一种对象类型,是否有明确定义的行为来说明它是否应该重复迭代。iterable[Symbol.iterator]iterableiterable[Symbol.iterator]() === iterabletrueiterable.nextiterable

JavaScript ECMAScript-6 可迭代

评论

1赞 VLAZ 1/5/2020
不是真的,但两者也不是真的等价。第一个对象是你强制迭代的对象,你使用迭代器并在它耗尽后尝试迭代。基本上,迭代器将表示它是否不再持有任何值,并且之后您不能继续。因此,在第一种情况下,如果你这样做了,你就不能做多次,因为迭代器已经完成。在第二种情况下,重复调用是可以的,因为你每次都采用一个新的迭代器(与第一个版本相同)。it = iterable[Symbol.iterator][...it][...generatorFn()]
2赞 jfriend00 1/5/2020
迭代器应该迭代,直到完成,然后完成。一旦报告,就完成了。可迭代对象应该能够根据需要提供新的迭代器以开始新的迭代。了解 an 和 . 之间的区别很重要。可迭代对象是可以获取迭代器的东西,并且可以使用该迭代器来遍历可迭代对象中的所有项目。{done: true}iterableiterator
0赞 nonopolarity 1/5/2020
@jfriend00所以你的意思是,一个可迭代对象每次都应该返回一个新的迭代器(理想情况下?在第二种情况下,是可迭代的,但显然它没有这样做iterable
0赞 nonopolarity 1/5/2020
@jfriend00仅仅因为在第二种情况下,可迭代对象来自生成器函数,那么这个可迭代对象就不完全是可迭代对象,而是一种不同类型的可迭代对象?我只知道如果一个对象符合可迭代协议,那么它就是一个正式的可迭代对象。没有“是的,它是一个可迭代的,但它是一种不同类型的可迭代”
1赞 jfriend00 1/5/2020
因此,显然只适用于假装是可迭代的迭代器。您可以看到,迭代器接口规范不需要(甚至提及)一个迭代器,只需返回自身即可伪装成可迭代器。这是内置迭代器决定自己做的事情。我敢肯定它有时可能很方便,但它确实混淆了可迭代对象和迭代器之间的界限(正如我们发现的那样)。[...iterator]

答:

1赞 Nina Scholz 1/5/2020 #1

iterableof 也是一个可迭代对象,也是一个 Generator 对象。const iterable = generatorFn();

Generator 对象由生成器函数返回,它符合可迭代协议和迭代器协议

该生成器遵循协议,仅使用可迭代对象运行一次。

评论

2赞 nonopolarity 1/5/2020
Mozilla 区分了生成器函数和生成器对象(或只是生成器)。我认为生成器也是一个可迭代的,否则将无法工作。如果它像鸭子一样嘎嘎叫,那么它就是鸭子。(或者至少它是庸医的)[...iterable]
1赞 jfriend00 1/5/2020
所以,这并不正确。 确实返回一个 Generator 对象。但是 Generator 对象既是 Iterable 又是 Iterator。它满足了这两个接口。您会看到这里在规范中表达的,其中说 Generator 对象是生成器函数的实例,并且符合 Iterator 和 Iterable 接口。这是故意这样做的,你甚至无法分辨出它是一个 Generator 对象。你可以检测到它既是一个 Iterable 又是一个 Iterator。generatorFn()
4赞 jfriend00 1/5/2020 #2

好的,我想我会总结一下我们在评论中学到的一些东西,并添加更多内容,然后通过写下您的具体问题的答案来结束。

[...x] 语法

该语法适用于支持接口的内容。而且,要支持可迭代接口,您所要做的就是支持该属性提供一个函数,该函数(在调用时)返回迭代器。[...x]iterablesSymbol.iterator

内置迭代器也是一个可迭代的

Javascript 中内置的所有迭代器都派生自同一个 IteratorPrototype。不需要迭代器执行此操作,这是内置迭代器做出的选择。

这个内置的也是一个 Iterable。它支持属性,这是一个功能。这是按规范进行的IteratorPrototypeSymbol.iteratorreturn this

这意味着所有内置迭代器(如 )都将使用语法。我不确定为什么这非常有用,但它肯定会导致对 Iterable 可以做什么和迭代器可以做什么的混淆,因为这些内置迭代器可以表现得像其中之一。someSet.values()[...x]

它会导致一些时髦的行为,因为如果你这样做:

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

第二个数组是空数组,因为这里只有一个迭代器。事实上。因此,第一个耗尽了迭代器。它坐在那里,无法再次迭代集合。这是因为内置迭代器的这种时髦行为,它们的行为是可迭代的,但只是.它们不会创建一个新的迭代器,该迭代器可以像使用实际的集合可迭代对象时那样再次迭代集合。每次访问时,此集合 iterable 都会返回一个全新的迭代器,如下所示:[...iter]x === ylet x = [...iter];donereturn thiss[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))

但是,如果通过向其添加适当的属性来使其成为 Iterable,它将作为[Symbol.iterator][...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 对象。根据规范,该 Generator 对象的行为既是 .故意没有办法判断这个迭代器/可迭代器是否来自生成器,这显然是故意的。调用代码只知道它是一个,生成器函数只是创建对调用代码透明的序列的一种方法。它的迭代方式与任何其他迭代器一样。IteratorIterableIterator/Iterable


两个迭代器的故事

在原始问题中,您显示了两个迭代器,一个重复工作,另一个不工作。这里有两件事在起作用。

首先,一些迭代器“消耗”了它们的序列,并且没有办法重复迭代相同的序列。这些将是制造的序列,而不是静态集合。

其次,在第一个代码示例中:

const iterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 3;
    yield 5;
  }
}

console.log([...iterable]);
console.log([...iterable]);
console.log([...iterable]);

单独的迭代器

可迭代就是可迭代。它不是一个迭代器。你可以通过调用 which is what 来请求它提供迭代器。但是,当你这样做时,它会返回一个全新的 Generator 对象,这是一个全新的迭代器。每次调用或导致调用 时,都会得到一个新的和不同的迭代器。iterable[Symbol.iterator]()[...iterable]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]);

这是不同的。你在这里所说的是我喜欢认为的.它同时实现了 和 接口,但是当您要求它提供 like does 时,它每次(本身)都会返回相同的对象。所以,每次你这样做时,它都在同一个迭代器上运行。但是该迭代器已耗尽,并且在您第一次执行后处于该状态。因此,后两个是空数组。迭代器没有更多可给出的了。iterablepseudo-iterableIterableIteratorIterator[...iterable][...iterable]done[...iterable][...iterable]

您的问题

是否有规则可以迭代,是否应该重复迭代?

没有。首先,一个最终达到状态的给定迭代器(非无限迭代器)一旦到达状态,就会给出任何结果。根据迭代器的定义。donedone

因此,表示某种静态序列的 that 是否可以重复迭代取决于它在每次被要求提供迭代器时提供的迭代器是否是新的和唯一的,我们在上面的两个例子中看到,它可以采用任何一种方式。IterableIteratorIterable

它每次都可以生成一个新的、唯一的迭代器,每次都会在序列中呈现新的迭代。

或者,每次都可以产生完全相同的结果。如果它这样做了,一旦该迭代器达到该状态,它就会卡在那里。IterableIteratordone

另请记住,某些 Iterable 表示可能不可重复的动态集合/序列。对于像 a 或 a 这样的东西来说,情况并非如此,但是更多自定义类型的 Iterables 在迭代时可能会基本上“消耗”它们的集合,并且当它完成时,即使你得到了一个新的迭代器,也没有更多了。SetMap

想象一下,一个迭代器给你一个价值 1 到 10 美元之间的随机金额的代码,并在每次你要求迭代器提供下一个值时从你的银行余额中减去它。在某个时候,你的银行余额达到,迭代器已经完成,即使获得一个新的迭代器,仍然需要处理相同的银行余额(没有更多的值)。这将是一个迭代器的例子 “消耗”值或一些资源,只是不可重复。$0$0

但是我想知道可迭代是一种对象类型,是否有明确定义的行为来说明它是否应该重复迭代。

不。它是特定于实现的,完全取决于你正在迭代的内容。使用静态集合(如 a 或 a 或 an),您可以获取新的迭代器并每次生成新的迭代。但是,我所说的 iterable(每次请求时返回相同的迭代器)或迭代对象(在迭代时序列被“消耗”)可能无法重复迭代。因此,它可以故意是任何一种方式。没有标准的方法。这取决于正在迭代的内容。SetMapArraypsuedo-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");
}
0赞 Sheraff 1/5/2020 #3

2021年更新

MDN 文档已更改,以反映耗尽的迭代器不应使自身再次可迭代。他们的示例现在达到并且不会重置。这与jfriend00的回答得出的结论是一致的。done: truethis.index = 0

此更正早于 MDN 迁移到 github,因此我没有更改的历史记录。我在下面留下之前的答案。


这个答案可能是错误的(见上面的更新)

MDN 文档有一个很好的实现,似乎表明我们更喜欢并且可迭代:

[Symbol.iterator]() {
  return {
    next: () => {
      if (this.index < this.data.length) {
        return {value: this.data[this.index++], done: false};
      } else {
        this.index = 0; //If we would like to iterate over this again without forcing manual update of the index
        return {done: true};
      }
    }
  };
}