在JavaScript ES6中,可迭代对象和迭代器有什么区别?

17

可迭代对象和迭代器是相同的吗,还是它们有所不同?

从规范(链接)来看,一个可迭代对象是一个对象,比如说obj,它具有一个obj[Symbol.iterator]函数属性,当调用该函数时,返回一个对象,该对象包含一个next方法,该方法可以返回一个{value: ___, done: ___}对象:

function foo() {
    let i = 0;
    const wah = {
        next: function() {
            if (i <= 2) return { value: (1 + 2 * i++), done: false }
            else return { value: undefined, done: true }
        }
    };
    return wah;     // wah is iterator
}

let bar = {}        // bar is iterable

bar[Symbol.iterator] = foo;

console.log([...bar]);             // [1, 3, 5]   
for (a of bar) console.log(a);     // 1 3 5 (in three lines)

以上代码中,bar是可迭代对象,wah是迭代器,next()则是迭代器接口。

因此,可迭代对象和迭代器是不同的东西。

现在,我们来看一个生成器和迭代器的常见示例:

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

const iter1 = gen1();

console.log([...iter1]);                           // [1, 3, 5]
for (a of iter1) console.log(a);                   // nothing

const iter2 = gen1();
for (a of iter2) console.log(a);                   // 1 3 5 (in three lines)

console.log(iter1[Symbol.iterator]() === iter1);   // true
在上面的例子中,gen1 是生成器,iter1 是迭代器,iter1.next() 会执行适当的操作。但是 iter1[Symbol.iterator] 提供的函数会返回一个函数,调用该函数会返回迭代器 iter1,因此在这种情况下,iter1 同时是可迭代对象和迭代器?同时,iter1 与上面的示例1不同,因为示例1中的可迭代对象可以使用 [...bar] 无限次地提供 [1, 3, 5],而 iter1 是一个可迭代对象,但由于它返回自身,即相同的迭代器,因此只能一次性提供 [1, 3, 5]
因此,我们可以说对于可迭代对象 bar[...bar] 可以多少次给出结果 [1, 3, 5],答案是取决于具体情况。可迭代对象和迭代器是否相同?答案是它们是不同的东西,但它们可以相同,当可迭代对象使用自身作为迭代器时是这样的。这个理解正确吗?

iter1 在这种情况下既是可迭代对象又是迭代器吗?”- 是的。所有本地迭代器也都是可迭代的,因为它们返回自身,所以你可以轻松地将它们传递给期望一个可迭代对象的结构中。 - Bergi
1
可能是Iterator和Iterable之间的区别的重复问题。 - Felix Kling
3个回答

14

是的,可迭代对象迭代器是不同的东西,但大多数迭代器(包括从JavaScript本身获得的所有迭代器,比如从Array.prototypekeysvalues方法或生成器函数生成的生成器)都继承自%IteratorPrototype% 原型对象,该对象具有以下Symbol.iterator方法:

[Symbol.iterator]() {
    return this;
}

所有标准迭代器都是可迭代的。这样你可以直接使用它们,或在期望可迭代对象(而不是迭代器)的for-of循环等中使用它们。
考虑数组的keys方法:它返回一个数组迭代器,访问数组的键(作为数字的索引)。请注意,它返回一个迭代器。但它的常见用法是:
for (const index of someArray.keys()) {
    // ...
}
for-of接受一个可迭代对象,而非迭代器,那么为什么它能正常工作呢?
因为迭代器本身也是可迭代的;Symbol.iterator只需返回this即可。
以下是我在第6章中使用的示例:如果你想循环遍历所有条目但跳过第一个条目,并且你不想使用slice 来切割子集,你可以获取迭代器,读取第一个值,然后将其移交给for-of循环:

const a = ["one", "two", "three", "four"];
const it = a[Symbol.iterator]();
// Skip the first one
it.next();
// Loop through the rest
for (const value of it) {
    console.log(value);
}

请注意,这是所有标准迭代器。有时候人们会展示手动编码迭代器的例子,像这样:

function range(start, end) {
    let value = start;
    let inc = start < end ? 1 : -1;
    return {
        next() {
            const done = value == end;
            const result = {done, value};
            if (!done) {
                value += inc;
            }
            return result;
        }
    };
}

// Works when used directly
const it = range(1, 5);
let result;
while (!(result = it.next()).done) {
    console.log(result.value);
}

// Fails when an iterable is expected
try {
    for (const value of range(1, 5)) {
        console.log(value);
    }
} catch (e) {
    console.error(e.message);
}

range函数返回的迭代器并不是可迭代对象,所以当我们试图使用for-of循环时会失败。
为了让它变成可迭代对象,我们需要做下面两件事中的一件:
  1. 在上面的答案开头添加Symbol.iterator方法;或者
  2. 使其继承自已经具有该方法的%IteratorPrototype%
不幸的是,TC39决定不提供直接获取%IteratorPrototype%对象的方式。虽然有一种间接的方式(从数组中获取迭代器,然后取其原型,该原型被定义为%IteratorPrototype%),但这种方式很麻烦。
但是没有必要像那样手动编写迭代器。只需使用生成器函数,因为它返回的生成器是可迭代的:

function* range(start, end) {
    let value = start;
    let inc = start < end ? 1 : -1;
    while (value !== end) {
        yield value;
        value += inc;
    }
}

// Works when used directly
const it = range(1, 5);
let result;
while (!(result = it.next()).done) {
    console.log(result.value);
}

// Also works when an iterable is expected
for (const value of range(1, 5)) {
    console.log(value);
}

相反,并非所有可迭代对象都是迭代器。数组和字符串、Map 和 Set 都是可迭代的,但不是迭代器。

3

这里是最简单的解释:

  • iterator - 任何拥有next函数来返回下一个值的对象
  • iterable - 任何拥有[Symbol.iterator]函数并返回一个iterator的对象

但当你拥有一个同时具备以上两个特点的对象,它被称为IterableIterator。例如,任何generator函数都会返回一个。

而一个IterableIterator的典型实现如下:

{
    [Symbol.iterator]() {
        return this; // returning iterator
    },
    next() {
        // return next value here
    }
} //=> IterableIterator

0

我发现有一些更精确的术语定义,这些是更明确的答案:

根据ES6规范MDN

当我们有

function* foo() {   // note the "*"
    yield 1;
    yield 3;
    yield 5;
}

foo 是一个称为生成器 函数 的函数。随后当我们调用它时

let bar = foo();

bar 是一个生成器 对象。而生成器对象符合可迭代协议和迭代器协议

更简单的版本是迭代器接口,它只是一个.next()方法。

可迭代协议是:对于对象objobj[Symbol.iterator]提供了一个“零参数函数,返回一个符合迭代器协议的对象”。

根据MDN链接的标题,似乎我们也可以称生成器对象为“生成器”。

请注意,在Nicolas Zakas的书《理解ECMAScript 6》中,他可能会将“生成器函数”简称为“生成器”,将“生成器对象”简称为“迭代器”。重点是,它们都与“生成器”相关--一个是生成器函数,一个是生成器对象或生成器。生成器对象符合可迭代协议和迭代器协议。
如果它只是符合迭代器协议的对象,则无法使用[...iter]for (a of iter)。它必须是符合可迭代协议的对象。

接着,在未来的JavaScript规范草案中,还有一个新的迭代器类。它有一个更大的接口,包括当前Array接口的forEachmapreduce方法以及新的方法,例如takedrop。当前的迭代器是指具有next接口的对象。

回答原始问题:迭代器和可迭代对象之间的区别是:迭代器是具有接口.next()的对象,而可迭代对象是一个对象obj,使得obj[Symbol.iterator]可以提供一个零参数函数,当调用时返回一个迭代器。

另外,生成器既是可迭代对象又是迭代器。


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