在JavaScript中,可迭代对象是否应该可以重复迭代?

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

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] 函数时,会返回相同的迭代器(即 iterable 自身。你可以尝试 iterable[Symbol.iterator] () === iterable ,它会返回trueiterable.next也是一个函数。因此,在这种情况下,iterable 是一个生成器对象、可迭代对象和迭代器,三者皆有)。但我想知道,作为一个对象类型的可迭代对象,是否存在定义明确的行为,决定它是否应该可以被重复迭代。


1
不完全相同,但两者也不是完全等价的。第一个是一个对象,你可以强制它进行迭代;而第二个则是一个*iterator(迭代器)*,在它被耗尽之后再试图迭代就不行了。基本上,一个迭代器会表示它是否还持有值,如果没有更多的值就无法继续执行。所以,在第一个情况中,如果你执行了 it = iterable[Symbol.iterator],那么你就不能做超过一次 [...it],因为迭代器已经完成了。在第二种情况下,重复调用[...generatorFn()]是可以的,因为你每次都会得到一个新的迭代器(和第一个版本一样)。 - VLAZ
2
一个迭代器应该在完成迭代后停止并且不再继续。一旦它报告 {done: true},就表示已经完成了。一个可迭代对象应该能够根据需要提供一个新的迭代器来开始新的迭代。理解“可迭代对象”和“迭代器”的区别非常重要。“可迭代对象”是指可以获取迭代器并使用该迭代器遍历可迭代对象中所有项的对象。 - jfriend00
@jfriend00 所以你的意思是一个可迭代对象应该每次返回一个新的迭代器(理想情况下)?在第二种情况下,iterable 是一个可迭代对象,但显然它没有这样做。 - nonopolarity
@jfriend00,你不能在一个迭代器上使用 [...iter] - nonopolarity
1
所以,[...iterator] 显然只适用于假装也是可迭代对象的迭代器。您可以查看iterator接口规范,它不要求(甚至不提到)迭代器通过返回自身来假装成可迭代对象。这是内置迭代器自己决定的事情。我确信有时候这可能很方便,但它肯定会混淆可迭代对象和迭代器之间的界线(正如我们发现的那样)。 - jfriend00
显示剩余9条评论
3个回答

4

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

[...x] 语法

[...x] 语法适用于支持 iterables 接口的对象。而要支持 iterable 接口,您只需支持 Symbol.iterator 属性以提供一个函数(调用时返回迭代器)即可。

内置迭代器也是可迭代的

Javascript 中内置的所有迭代器都源自同一个IteratorPrototype。不要求迭代器这样做,这是内置迭代器所做的选择。

此内置的 IteratorPrototype 也是可迭代的。它支持 Symbol.iterator 属性,该属性是一个函数,只需执行 return this 即可。这是规范要求的。

这意味着所有内置迭代器,如 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);

第二个[...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]属性将其变成可迭代对象,它就可以像[...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]一样工作。

生成器

当调用生成器函数时,它会返回一个生成器对象。根据规范,该生成器对象被视为迭代器和可迭代对象。故意没有办法告诉迭代器/可迭代对象是否来自生成器,这显然是有意为之。调用代码只知道它是一个迭代器/可迭代对象,生成器函数只是创建序列的一种透明方式。它像任何其他迭代器一样迭代。


两个迭代器的故事

在您的原始问题中,您展示了两个迭代器,一个可以重复工作,而另一个则不能。这里有两件事情要处理。

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

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

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

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

分离迭代器

可迭代对象是一个可迭代的集合。它不是一个迭代器。您可以通过调用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]);

这是不同的。 在这里你所称呼的iterable是我喜欢称之为伪可迭代对象(pseudo-iterable)。它实现了 Iterable和Iterator接口,但是当你像[...iterable]一样请求Iterator时,它每次都会返回相同的对象(也就是自身)。因此,每次执行[...iterable]时,它都在操作同一个iterator。但是iterator已经被耗尽,并且在第一次执行[...iterable]后处于done状态。所以,接下来的两个[...iterable]返回的将是空数组。iterator已经没有更多可以给出的值了。 你的问题 是否有规则规定可迭代对象是否应该可以重复迭代?
并没有。首先,任何达到done状态的iterator(一个非无限iterator)一旦达到done状态就不再提供任何结果,这是iterator的定义。
因此,表示某种静态序列的Iterable是否可以重复迭代取决于每次请求Iterator时它提供的Iterator是否是新的和唯一的。正如上面的两个例子所示,一个Iterable可以采用不同的方式。
它可以每次产生一个新的、独特的iterator,每次都提供一个新的遍历序列的机会。
或者,一个Iterable可以每次都产生完全相同的Iterator。如果是这样,一旦iterator达到done状态,就会卡在那里。
还要注意,一些Iterable表示动态集合/序列,可能不可重复。这对于像Set或Map这样的东西并不是真的,但更定制化类型的Iterable可能会在迭代时“消耗”其集合,并且一旦完成,即使您获得一个新的新鲜Iterator,也没有更多了。
想象一下一个iterator,每次交付给你一个价值介于1美元和10美元之间的代码,并从您的银行余额中扣除相应的金额。在某个时候,您的银行余额会变为0美元,而该iterator已经完成,即使获得一个新的iterator,仍然必须处理相同的0美元银行余额(没有更多的值)。这是一个iterator"消耗"值或某些资源,无法重复遍历的例子。
但我想知道,作为对象类型的iterable是否有明确定义的行为,规定它应该还是不应该可以重复迭代。

不是的。这取决于你要迭代的内容和实现方式。对于像SetMap或者Array这样的静态集合,你可以获取一个新的迭代器,并在每次迭代时生成一个全新的迭代。但是,像我所说的"伪可迭代"(每次请求返回相同迭代器的可迭代对象)或者在迭代时已被"消耗"掉的可迭代对象可能无法重复进行迭代。因此,它可以有意地是两种情况中的任何一种。没有标准方法。这取决于正在迭代的内容。

测试你所拥有的内容

以下是几个有用的测试,可帮助您更好地理解:

// 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");
}

1

2
Mozilla区分生成器函数和生成器对象(或仅称为生成器)。我认为生成器也是可迭代的,否则[...iterable]将无法工作。如果它像鸭子一样嘎嘎叫,那么它就是鸭子(或者至少可以模仿鸭子叫声)。 - nonopolarity
1
所以,这并不完全正确。generatorFn()确实返回一个Generator对象。但是,Generator对象既是Iterable,也是Iterator。它满足这两个接口。你可以在规范中看到这一点,其中说:“Generator对象是生成器函数的实例,并符合Iterator和Iterable接口。”这是故意这样做的,你甚至不能告诉它是一个Generator对象。你只能检测到它同时是Iterable和Iterator。 - jfriend00

0

2021 更新

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

这个更正早于 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};
      }
    }
  };
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接