In JavaScript ES6, what is the difference between an iterable and iterator?

不打扰是莪最后的温柔 提交于 2020-12-25 01:55:19

问题


Is an iterable the same as an iterator, or are they different?

It seems, from the specifications, an iterable is an object, say, obj, such that obj[Symbol.iterator] refers to a function, so that when invoked, returns an object that has a next method that can return a {value: ___, done: ___} object:

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)

So in the code above, bar is the iterable, and wah is the iterator, and the next() is the iterator interface.

So, iterable and iterator are different things.

Now, however, in a common example of generator and iterator:

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

In the case above, gen1 is the generator, and iter1 is the iterator, and iter1.next() will do the proper job. But iter1[Symbol.iterator] does give a function that, when invoked, gives back iter1, which is an iterator. So iter1 is both an iterable and iterator in this case?

Besides, iter1 is different from the example 1 above, because the iterable in example 1 can give [1, 3, 5] as many times as wanted using [...bar], while iter1 is an iterable, but since it returns itself, which is the same iterator every time, will only give [1, 3, 5] once.

So we can say, for an iterable bar, how many times can [...bar] give the result [1, 3, 5] -- and the answer is, it depends. And is iterable the same as an iterator? And the answer is, they are different things, but they can be the same, when the iterable uses itself as the iterator. Is that correct?


回答1:


Yes, iterables and iterators are different things, but most iterators (including all of the ones you get from JavaScript itself, such as from the keys or values methods on Array.prototype or generators from generator functions) inherit from the %IteratorPrototype% object, which has a Symbol.iterator method like this:

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

The result is that all standard iterators are also iterables. That's so you can use them directly, or use them in for-of loops and such (which expect iterables, not iterators).

Consider the keys method of arrays: It returns an array iterator that visits the array's keys (its indexes, as numbers). Note that it returns an iterator. But a common use of it is:

for (const index of someArray.keys()) {
    // ...
}

for-of takes an iterable, not an iterator, so why does that work?

It works because the iterator is also iterable; Symbol.iterator just returns this.

Here's an example I use in Chapter 6 of my book: If you wanted to loop over all entries but skip the first one and you didn't want to use slice to slice off the subset, you can get the iterator, read the first value, then hand off to a for-of loop:

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

Note that this is all standard iterators. Sometime people show examples of manually-coded iterators like this:

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

The iterator returned by range there is not an iterable, so it fails when we try to use it with for-of.

To make it iterable, we'd need to either:

  1. Add the Symbol.iterator method at the beginning of the answer above to it, or
  2. Make it inherit from %IteratorPrototype%, which already has that method

Sadly, TC39 decided not to provide a direct way to get the %IteratorPrototype% object. There's an indirect way (getting an iterator from an array, then taking its prototype, which is defined to be %IteratorPrototype%), but it's a pain.

But there's no need to write iterators manually like that anyway; just use a generator function, since the generator it returns is iterable:

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

In contrast, not all iterables are iterators. Arrays are iterable, but not iterators. So are strings, Maps, and Sets.




回答2:


I found that there are some more precise definitions of the terms, and these are the more definitive answers:

According to the ES6 Specs and MDN:

When we have

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

foo is called generator function. And then when we have

let bar = foo();

bar is a generator object. And a generator object conforms to both the iterable protocol and the iterator protocol.

The simpler version is the iterator interface, which is just a .next() method.

The iterable protocol is: for the object obj, obj[Symbol.iterator] gives a "zero arguments function that returns an object, conforming to the iterator protocol".

By the title of the MDN link, it also seems we can also just call a generator object a "generator".

Note that in Nicolas Zakas's book Understanding ECMAScript 6, he probably loosely called a "generator function" as a "generator", and a "generator object" as an "iterator". The take away point is, they are really both "generator" related -- one is a generator function, and one is a generator object, or generator. The generator object conforms to both the iteratable protocol and the iterator protocol.

If it is just an object conforming to the iterator protocol, you cannot use [...iter] or for (a of iter). It has to be an object that conforms to the iterable protocol.

And then, there is also a new Iterator class, in a future JavaScript specs that is still in a draft. It has a larger interface, including methods such as forEach, map, reduce of the current Array interface, and new ones, such as and take, and drop. The current iterator refers to the object with just the next interface.

To answer the original question: what is the difference between an iterator and an iterable, the answer is: an iterator is an object with the interface .next(), and an iterable is an object obj such that obj[Symbol.iterator] can give a zero-argument function that, when invoked, returns an iterator.

And a generator is both an iterable and iterator, to add to that.



来源:https://stackoverflow.com/questions/59458257/in-javascript-es6-what-is-the-difference-between-an-iterable-and-iterator

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!