1

My idea is that, if obj is an iterable, then each time the obj[Symbol.iterator] function is invoked, it spits out a brand new iterator. Now, aMap.entries() gives back an iterable, so:

map = new Map([["a", 3], ["b", 'hi'], ["c", true]]);

for (const e of map.entries()) {
    console.log(e);
}

for (const e of map.entries()) {
    console.log(e);
}

const someEntries = map.entries();

console.log(someEntries.next());

console.log(someEntries[Symbol.iterator]);

console.log(someEntries[Symbol.iterator]().next());

console.log(someEntries[Symbol.iterator]().next());

console.log(someEntries[Symbol.iterator]().next());

console.log(someEntries[Symbol.iterator]().next());

So that's why the for-of loop above can print out all key-value pairs twice.

However, the last 6 lines of the code, when I invoke that function and use next(), it gave a "continuation" of the next() function... so it is not a brand new iterator but the old one. So this conflicts with the idea that "each time the obj[Symbol.iterator] function is invoked, it spits out a brand new iterator".

So what does that mean? Each time the obj[Symbol.iterator] function is invoked, it spits out a iterator, but it can be an old iterator, or it can be a new iterator? There is no guarantee?

For example, the following code shows that it spits out a new iterator each time:

const arr = [1, 3, 5];

console.log(arr[Symbol.iterator]().next());

console.log(arr[Symbol.iterator]().next());

console.log(arr[Symbol.iterator]().next());
nonopolarity
  • 146,324
  • 131
  • 460
  • 740
  • 2
    The problem here is that iterators are *themselves* iterable. But the iterable of an iterator is the iterator itself... It's somewhat of a convenience when you want to work with "something that can be iterated over" but don't want to define whether it's an itera**tor** or itera**ble**. They are known as "iterable iterators". I hope I didn't reach [semantic satiation](https://en.wikipedia.org/wiki/Semantic_satiation). – VLAZ Feb 02 '21 at 14:40
  • If you use `for..of` on `someEntries`, you can't do that again. You can do that with `map.entries()` because it returns a new iterator object. – adiga Feb 02 '21 at 14:44
  • @VLAZ are you saying the iterator that are also iterable has a name: "iterable iterator", and these "iterable iterator" typically spit itself out instead of spitting out a new iterator? If it is like that then maybe the JS ES6 docs can specify it more clearly: iterator is iterator, iterable is iterable, but there is a third type: iterable iterator – nonopolarity Feb 02 '21 at 14:57
  • I'm not sure if it's an "official name" but it's pretty common. And it's just self-descriptive - an itera**ble** has a `Symbol.iterator` method; an itera**tor** has a `next` method. Iterable iterators possess both a `Symbol.iterator` method and a `next` method - so it fulfils the requirements for both. As it happens, this particular iterable (the default implementation of `.entries()`) returns itself as the iterator. – VLAZ Feb 02 '21 at 15:02
  • I guess to bring it back to the question at hand - there is no guarantee that an *iterable* is always going to produce an iterator that is distinct. There is nothing requiring that. An iterable that returns the same iterator every time and that iterator can be exhausted once until forever and ever is still valid. – VLAZ Feb 02 '21 at 15:09
  • I think if all iterable iterator behave like this, it'd be good if the docs can specify such behavior – nonopolarity Feb 02 '21 at 15:16
  • @nonopolarity no, not *all* iterable iterators. Again, that's just two interfaces, so they can be implemented however you want. `{ next() { return { value: 4, done: false }; }, [Symbol.iterator]() { return [1, 2, 3].values(); } }` is a valid implementation for an iterable iterator. It has a `next()` method that fulfills the iterator contract and it has a `Symbol.iterator` method that returns an iterable. However, repeat calls to `Symbol.iterator` will not return the same iterator back, so they can all be independently exhausted. – VLAZ Feb 03 '21 at 07:04
  • Closely related: [In JavaScript, should an iterable be repeatedly iterable?](https://stackoverflow.com/q/59595512/1048572) – Bergi Apr 11 '21 at 16:34

2 Answers2

2

Your initial view, that the @@iterator method of an iterable should yield a new iterator every time it's called, is correct.

You are confused by a different phenomenon.

When you call the @@iterator method of a map (or Map#entries, both do the same), you get a brand-new iterator, that you can consume explicitly.

If you use for..of on a map, it will internally call the @@iterator method, and do the same.

map = new Map([["a", 3], ["b", 'hi'], ["c", true]]);

//This will use @@iterator:
for (const e of map) {
    console.log(e);
}

//Or do the same manually:
const someEntries = map.entries();

console.log(someEntries.next().value);
console.log(someEntries.next().value);
console.log(someEntries.next().value);

However, there are cases when you want to mix the two and use both of these. For example:

map = new Map([["a", 3], ["b", 'hi'], ["c", true]]);

const someEntries = map.entries();

//Get the first
console.log('The first element is:', someEntries.next().value);

//Consume the rest
for (const e of someEntries) {
    console.log(e);
}

However, to do that, we had to pass our iterator to for..of (if we had passed our map, it would've created a new iterator, and started over). It will (as always) try to call its @@iterator method (passing an iterator without an @@iterator method to for..of will result in an error). So, for that to work, all native iterators must have their own @@iterator method, that, most of the times returns the iterator object itself, and that's what makes iterators iterable by native constructs.

Therefore, when you call @@iterator on an iterator, it won't start over, but rather consume itself further.

This snippet might make it clearer:

map = new Map([["a", 3], ["b", 'hi'], ["c", true]]);

const someEntries = map.entries();

console.log(someEntries[Symbol.iterator] === map[Symbol.iterator]) //false, the iterator doesn't share its @@iterator method with the map
console.log(someEntries[Symbol.iterator]() === someEntries) //true, calling @@iterator on an iterator returns itself

const stillSomeEntries = someEntries[Symbol.iterator]() //The same iterator as someEntries

const someOtherEntries = map.entries();

console.log(someEntries.next().value);
console.log(stillSomeEntries.next().value);
console.log(someEntries.next().value);

console.log(someOtherEntries.next().value);
console.log(someOtherEntries.next().value);
console.log(someOtherEntries.next().value);
FZs
  • 16,581
  • 13
  • 41
  • 50
1

So what does that mean? There is no guarantee?

Yes. Most iterables do create a new iterator object, but not all of them. Iterators usually are iterable them­selves (so that you can write const entriesIter = map.entries(); for (const e of entriesIter) …), and those usually return the same iterator every time - themselves: entriesIter[Symbol.iterator]() === entriesIter.

However, map.entries(), map[Symbol.iterator]() or arr[Symbol.iterator]() do return new iterators.

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
Bergi
  • 630,263
  • 148
  • 957
  • 1,375