DEV Community

Cover image for Iterables & Iterators - An In-Depth JavaScript Tutorial
mpodlasin
mpodlasin

Posted on • Updated on • Originally published at mpodlasin.com

Iterables & Iterators - An In-Depth JavaScript Tutorial

This article is an in-depth introduction to iterables and iterators in JavaScript. My main motivation for writing it was to prepare ourselves for learning generators next. In fact, later I plan to do some experimental stuff, like combining generators with React hooks.

As a matter of fact, I planned to start with the generators article, but it quickly became obvious to me, that they are tough to explain without having a solid understanding of iterables & iterators first.

That's why in this article we will focus on iterables & iterators only. We will assume no previous knowledge about them, but at the same time, we will go fairly in-depth. So if you know something about iterables & iterators, but you still don't feel fully comfortable using them, this article should fix that.

Introduction

As you've noticed, we are talking about iterables and iterators. They are related, but distinct concepts, so while reading the article make sure to keep tabs on which one we are talking about at any given moment.

Let's begin with iterables. What are they? An iterable is basically something that can be iterated over, like so:

for (let element of iterable) {
    // do something with an element
}
Enter fullscreen mode Exit fullscreen mode

Note that we are talking only about for ... of loops here, which were introduced in ES6. for ... in loops are an older construct and we will not use it at all in this article.

You might now think, "okay, this iterable variable is simply an array!". And indeed, arrays are iterables. But even currently in native JavaScript, there are other data structures that we could use in a for ... of loop. In other words, there are more iterables in native JavaScript than just arrays.

For example, we can iterate over ES6 Maps:

const ourMap = new Map();

ourMap.set(1, 'a');
ourMap.set(2, 'b');
ourMap.set(3, 'c');

for (let element of ourMap) {
    console.log(element);
}
Enter fullscreen mode Exit fullscreen mode

This code will print out:

[1, 'a']
[2, 'b']
[3, 'c']
Enter fullscreen mode Exit fullscreen mode

So variable element in the code above stores in each iteration step an array of two elements. The first element is a key, and the second element is a value.

The fact that we could use for ... of loop to iterate over Map, proves to us that Maps are iterables. Once again - only iterables can be used in for ... of loops. So if something works with that loop - it is an iterable.

Funnily enough, Map constructor itself optionally accepts an iterable of key-value pairs. So this is an alternative way to construct the same Map as before:

const ourMap = new Map([
    [1, 'a'],
    [2, 'b'],
    [3, 'c'],
]);
Enter fullscreen mode Exit fullscreen mode

And since - as we've just noted - Map itself is an iterable, we can create copies of Maps extremely easily:

const copyOfOurMap = new Map(ourMap);
Enter fullscreen mode Exit fullscreen mode

We have now two distinct maps, although they are storing the same values under the same keys.

So we've seen two examples of iterables so far - an array and an ES6 Map.

But we still didn't explain how do they possess this magic power of being able to be iterated over.

The answer is simple - they have iterators associated with them. Read that carefully. Itera*tors, not iterables*.

In what way an iterator is associated with it's iterable? An iterable object simply has to have a function under its Symbol.iterator property. This function, when called, should return an iterator for that object.

For example, we can retrieve an array's iterator like so:

const ourArray = [1, 2, 3];

const iterator = ourArray[Symbol.iterator]();

console.log(iterator);
Enter fullscreen mode Exit fullscreen mode

This code will print Object [Array Iterator] {} to the console.

So we know that our array has an associated iterator and that this iterator is some kind of object.

What is an iterator then?

It's fairly simple. An iterator is just an object that has a next method. This method, when called, should return:

  • next value in a sequence of values,
  • information whether the iterator has finished producing values or not.

Let's test it, by calling the next method of the iterator of our array:

const result = iterator.next();

console.log(result);
Enter fullscreen mode Exit fullscreen mode

We will see an object { value: 1, done: false } printed in the console.

The first element of the array we created was 1, so it appeared as the value here. We also got information that the iterator is not done yet, meaning we still can call the next function and expect to see some values.

Let's do it! In fact, let's call next two more times:

console.log(iterator.next());
console.log(iterator.next());
Enter fullscreen mode Exit fullscreen mode

Unsurprisingly, we get { value: 2, done: false } and { value: 3, done: false } printed, one after another.

But our array had only 3 elements. So what happens, if we try to call next yet again?

console.log(iterator.next());
Enter fullscreen mode Exit fullscreen mode

This time we see { value: undefined, done: true } printed. This is information for us that the iterator has finished. There is no point calling next again. In fact, if we do so, we will receive the same { value: undefined, done: true } object over and over again. done: true is a sign for us to stop the iteration.

Now we can understand what for ... of loop does under the hood.

  • First [Symbol.iterator]() method is called to get an iterator,
  • next method is being called on that iterator in a loop until we get done: true,
  • after each call to next, value property is used in the loop's body.

Let's write all that in code:

const iterator = ourArray[Symbol.iterator]();

let result = iterator.next();

while (!result.done) {
    const element = result.value;

    // do some something with element

    result = iterator.next();
}
Enter fullscreen mode Exit fullscreen mode

All this code is directly equivalent to:

for (let element of ourArray) {
    // do something with element
}
Enter fullscreen mode Exit fullscreen mode

You can make sure that's the case by, for example, placing console.log(element) in place of // do something with element comment.

Creating our own iterator

So we know what the iterables and iterators are. The question then becomes - is it possible to write our own instances of them?

Absolutely!

There is nothing magical about iterators. They are just objects with a next method, which behaves in a specified way.

We've said which native JS values are iterables. We haven't mention objects there. Indeed, they are not iterables natively. Take an object like this:

const ourObject = {
    1: 'a',
    2: 'b',
    3: 'c'
};
Enter fullscreen mode Exit fullscreen mode

When we try to iterate over that object with for (let element of ourObject), we will get an error, stating that object is not iterable.

So let's practice writing custom iterators by making such an object an iterable!

In order to do that, we would have to patch Object prototype with our custom [Symbol.iterator]() method. Since patching prototypes is a bad practice, let's just create a custom class, extending Object:

class IterableObject extends Object {
    constructor(object) {
        super();
        Object.assign(this, object);
    }
}
Enter fullscreen mode Exit fullscreen mode

The constructor of our class simply takes a regular object and copies its properties onto an iterable one (although it's not really iterable yet!).

So we will be creating an interable object like this:

const iterableObject = new IterableObject({
    1: 'a',
    2: 'b',
    3: 'c'
})
Enter fullscreen mode Exit fullscreen mode

In order to make the IterableObject class actually iterable, it needs to have a [Symbol.iterator]() method. Let's add it then.

class IterableObject extends Object {
    constructor(object) {
        super();
        Object.assign(this, object);
    }

    [Symbol.iterator]() {

    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can start writing an actual iterator!

We already know that it has to be an object, which has a next method on it. So let's start with that.

class IterableObject extends Object {
    // same as before

    [Symbol.iterator]() {
        return {
            next() {}
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

After every call to next, we have to return an object of shape { value, done }. Let's do just that, with some dummy values.

class IterableObject extends Object {
    // same as before

    [Symbol.iterator]() {
        return {
            next() {
                return {
                    value: undefined,
                    done: false
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Given an iterable object:

const iterableObject = new IterableObject({
    1: 'a',
    2: 'b',
    3: 'c'
})
Enter fullscreen mode Exit fullscreen mode

we would like to print it's key-value pairs, similarily to what iterating over ES6 Map did:

['1', 'a']
['2', 'b']
['3', 'c']
Enter fullscreen mode Exit fullscreen mode

So in our custom iterator, under the value property we want to place an array [key, valueForThatKey].

Note that this - compared to the previous steps of the example - is our own design decision. If we wanted to write an iterator that returned only keys or only property values - we might do that as well, and it would be perfectly fine. We simply ourselves decided to return key-value pairs.

So we will need arrays of shape [key, valueForThatKey]. The easiest way to obtain them is simply to use the Object.entries method.

We can use it just before creating an iterator object in the [Symbol.iterator]() method:

class IterableObject extends Object {
    // same as before

    [Symbol.iterator]() {
        // we made an addition here
        const entries = Object.entries(this);

        return {
            next() {
                return {
                    value: undefined,
                    done: false
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The iterator returned in that method will have an access to the entries variable thanks to a JavaScript closure.

But we also need some kind of state variable. It will tell us which key-value pair should be returned in a current next call. So let's add that as well.

class IterableObject extends Object {
    // same as before

    [Symbol.iterator]() {
        const entries = Object.entries(this);
        // we made an addition here
        let index = 0;

        return {
            next() {
                return {
                    value: undefined,
                    done: false
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Note how we declared index variable with a let because we know that we plan to update its value after each next call.

We are now ready to return an actual value in the next method:

class IterableObject extends Object {
    // same as before

    [Symbol.iterator]() {
        const entries = Object.entries(this);
        let index = 0;

        return {
            next() {
                return {
                    // we made a change here
                    value: entries[index],
                    done: false
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This was easy. We just used both entries and index variables to access a proper key-value pair from the entries array.

Now we have to deal with that done property because currently, it will be always set to false.

We could keep another variable - alongside entries and index - and update it after every next call. But there is an even easier way. We can simply check if index already went out of bounds of the entries array:

class IterableObject extends Object {
    // same as before

    [Symbol.iterator]() {
        const entries = Object.entries(this);
        let index = 0;

        return {
            next() {
                return {
                    value: entries[index],
                    // we made a change here
                    done: index >= entries.length
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Indeed, our iterator is done when the index variable is equal to the length of entries or is bigger.

For example, if entries has length 3, it has values under indexes 0, 1, and 2. So when the index variable is 3 (equal to the length), or bigger, it means there are no more values to get. That's when we are done.

This code almost works. There is only one more thing we need to add.

The index variable starts with a value 0, but... we are never updating it!

It's actually kind of tricky because we should update it after we return { value, done }. But when we return it, the next method stops running immediately, even if there is some code after the return statement.

We can however create the { value, done } object, store it in a variable, update the index and just then return the object:

class IterableObject extends Object {
    // same as before

    [Symbol.iterator]() {
        const entries = Object.entries(this);
        let index = 0;

        return {
            next() {
                const result = {
                    value: entries[index],
                    done: index >= entries.length
                };

                index++;

                return result;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

After all these changes, this is how our IterableObject class looks so far:

class IterableObject extends Object {
    constructor(object) {
        super();
        Object.assign(this, object);
    }

    [Symbol.iterator]() {
        const entries = Object.entries(this);
        let index = 0;

        return {
            next() {
                const result = {
                    value: entries[index],
                    done: index >= entries.length
                };

                index++;

                return result;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This code works perfectly fine, but it became a bit convoluted. There is actually a smarter (but less obvious) way to deal with having to update index after creating the result object. We can simply initialize index with -1!

Then, even though the index update happens before returning the object from next, everything will work just fine, because the first update will bump -1 to 0.

So let's do just that:

class IterableObject extends Object {
    // same as before

    [Symbol.iterator]() {
        const entries = Object.entries(this);
        let index = -1;

        return {
            next() {
                index++;

                return {
                    value: entries[index],
                    done: index >= entries.length
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, now we don't have to juggle the order of creating the result object and updating index. That's because we are starting with -1. During the first next call, index will be updated to 0 and then we will return the result.

During the second call, index will be updated to 1 and we will return another result, etc...

So everything will work just as we wanted, and the code looks now much simpler than the previous version.

How we can test if it really works properly? We could manually run [Symbol.iterator]() method to create an iterator instance, then directly test the results of next calls, etc.

But there is a much simpler way! We've said that every iterable can be plugged into for ... of loop! So let's do just that and log the values returned by our custom iterable:

const iterableObject = new IterableObject({
    1: 'a',
    2: 'b',
    3: 'c'
});

for (let element of iterableObject) {
    console.log(element);
}
Enter fullscreen mode Exit fullscreen mode

It works! You will see the following result printed in the console:

[ '1', 'a' ]
[ '2', 'b' ]
[ '3', 'c' ]
Enter fullscreen mode Exit fullscreen mode

That's exactly what we wanted!

Isn't this cool? We've started with objects not being able to be used in for ... of loops because natively they don't have built-in iterators. But we created a custom IterableObject, which does have an associated iterator, which we have written by hand.

I hope that by now you can see and appreciate the power of iterables and iterators. It's a mechanism that allows your own data structures to cooperate with JS features like for ... of loops, in a way indistinguishable from the native data structures! That's very powerful and in certain situations, it can vastly simplify the code, especially if you plan to do iterations on your data structures often.

On top of that, we can customize what exactly such iteration will return. We've settled on returning key-value pairs from our iterator. But what if we cared only about the values themselves? No problem! We can just rewrite our iterator:

class IterableObject extends Object {
    // same as before

    [Symbol.iterator]() {
        // changed `entries` to `values`
        const values = Object.values(this);
        let index = -1;

        return {
            next() {
                index++;

                return {
                    // changed `entries` to `values`
                    value: values[index],
                    // changed `entries` to `values`
                    done: index >= values.length
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And that's it!

If we run for ... of loop after this change, we will see the following output in the console:

a
b
c
Enter fullscreen mode Exit fullscreen mode

So we really returned only the objects values, just as we wanted.

This proves how flexible your custom iterators can be. You can really make them return whatever you wish.

Iterators as... iterables

You will see people very often confusing iterators and iterables.

That's a mistake and I was trying to carefully differentiate between the two in this article, but I think I know one of the main reasons why people confuse them so often.

It turns out that iterators... are sometimes iterables as well!

What does it mean? We said that an iterable is an object that has an iterator associated with it.

It turns out that every native JavaScript iterator also has a [Symbol.iterator]() method, returning yet another iterator! This - according to our previous definition - makes that first iterator an iterable.

We can check that it is true, by taking an iterator returned from an array and calling [Symbol.iterator]() on it once more:

const ourArray = [1, 2, 3];

const iterator = ourArray[Symbol.iterator]();

const secondIterator = iterator[Symbol.iterator]();

console.log(secondIterator);
Enter fullscreen mode Exit fullscreen mode

After running this code, you will see Object [Array Iterator] {}.

So not only our iterator has another iterator associated with it, but we also see that it is again an array iterator.

In fact, if we compare those two iterators with ===, it turns out that this is simply exactly the same iterator:

const iterator = ourArray[Symbol.iterator]();

const secondIterator = iterator[Symbol.iterator]();

// logs `true`
console.log(iterator === secondIterator);
Enter fullscreen mode Exit fullscreen mode

This behavior of an iterator being its own iterator might seem strange at the beginning.

But it's actually fairly useful.

You cannot plug a bare iterator into the for ... of loop. for ... of accepts only an iterable - that is an object with a [Symbol.iterator]() method.

However, an iterator being its own iterator (and hence an iterable) mitigates that problem. Since native JavaScript iterators do have [Symbol.iterator]() methods on them, you can pass them to for ... of loops directly without thinking twice.

So because of that feature, both:

const ourArray = [1, 2, 3];

for (let element of ourArray) {
    console.log(element);
}
Enter fullscreen mode Exit fullscreen mode

and:

const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();

for (let element of iterator) {
    console.log(element);
}
Enter fullscreen mode Exit fullscreen mode

work without any problems and do exactly the same thing.

But why would you even want to use an iterator directly in a for ... of loop like that? The answer is simple - it turns out that sometimes it is simply unavoidable.

First of all, you might want to create an iterator without any iterable to which it belongs. We will see such example later, and it's actually not that rare to create such "bare" iterators. Sometimes an iterable itself just isn't needed.

And it would be very akward if having a bare iterator meant you couldn't just consume it via for ... of. It's of course always possible to do it manually with a next method and, for example, a while loop, but we've seen that it requires quite a lot of typing and boilerplate.

It's simple - if you want to avoid that boilerplate and use your iterator in a for ... of loop, you have to make it an iterable as well.

On the other hand, you will also quite often receive iterators from methods other than [Symbol.iterator](). For example, ES6 Map has entries, values and keys methods. All of them return iterators.

If native JavaScript iterators weren't iterables as well, you couldn't just use those methods directly in for ... of loops like that:

for (let element of map.entries()) {
    console.log(element);
}

for (let element of map.values()) {
    console.log(element);
}

for (let element of map.keys()) {
    console.log(element);
}
Enter fullscreen mode Exit fullscreen mode

The code above works, because iterators returned by the methods are also iterables.

If they weren't, we would have to, for example, awkardly wrap a result from map.entries() call in some kind of a dummy iterable. Luckily we don't have to, and we can just use those methods directly, without worrying too much about it.

For those reasons, it is a good practice to make your custom iterators iterables as well. Especially if they will be returned from some methods other than [Symbol.iterator]().

And it's actually very simple to make an iterator an iterable. Let's do that with our IterableObject iterator.

class IterableObject extends Object {
    // same as before

    [Symbol.iterator]() {
        // same as before

        return {
            next() {
                // same as before
            },

            [Symbol.iterator]() {
                return this;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we just created a [Symbol.iterator]() method under the next method.

We've made this iterator it's own iterator by simply returning this - so it just returned itself. We've seen that that's exactly how the array iterator behaved.

That's enough to make sure that our iterator works with for ... of loops, even when used in them directly.

State of an iterator

It should be fairly clear by now that each iterator has a state associated with it.

For example in our IterableObject iterator, we kept the state - an index variable - as a closure.

After each iteration step, that index was updated.

So what happens after the iteration process ends? It's simple - the iterator becomes useless and we can (and should!) discard it.

We can doublecheck that this happens even with iterators of native JavaScript objects.

We will take an iterator of an array and try to run it in a for ... of loop twice.

const ourArray = [1, 2, 3];

const iterator = ourArray[Symbol.iterator]();

for (let element of iterator) {
    console.log(element);
}

for (let element of iterator) {
    console.log(element);
}
Enter fullscreen mode Exit fullscreen mode

You might expect to see numbers 1, 2, 3 appearing in the console twice. But this is not what happens. The result is still just:

1
2
3
Enter fullscreen mode Exit fullscreen mode

But why?

We can discover that, by trying to call next manually, after the loop finishes:

const ourArray = [1, 2, 3];

const iterator = ourArray[Symbol.iterator]();

for (let element of iterator) {
    console.log(element);
}

console.log(iterator.next());
Enter fullscreen mode Exit fullscreen mode

The last log prints { value: undefined, done: true } to the console.

Aaah. So after the loop finishes, the iterator is now in its "done" state. From now on it always return a { value: undefined, done: true } object.

Is there a way to "reset" the state of this iterator, in order to use it in a for ... of loop second time?

In some cases perhaps, but there is really no point. This is exactly why [Symbol.iterator] is a method and not just a property. We can simply call that method again to obtain another iterator:

const ourArray = [1, 2, 3];

const iterator = ourArray[Symbol.iterator]();

for (let element of iterator) {
    console.log(element);
}

const secondIterator = ourArray[Symbol.iterator]();

for (let element of secondIterator) {
    console.log(element);
}
Enter fullscreen mode Exit fullscreen mode

Now it works as we would expect.

Right now you should be able to understand why looping over an array directly multiple times works:

const ourArray = [1, 2, 3];

for (let element of ourArray) {
    console.log(element);
}

for (let element of ourArray) {
    console.log(element);
}
Enter fullscreen mode Exit fullscreen mode

That's because each of those for ... of loops uses a different iterator! After an iterator is done and a loop ends, that iterator is never used again.

Iterators vs arrays

Because we use iterators (although indirectly) in for ... of loops, they might look to you deceivingly similar to arrays.

But there are two important distinctions to be made between the iterators and the arrays.

Both of them have to do with the concept of eager and lazy values.

When you create an array, at any given moment it has a specific length and its values are already initialized.

I mean, sure, you can create an array wihout any values inside, but that's not what we mean here.

We mean that it is impossible to create an array that initializes its value only after you attempt to access that value by writing array[someIndex]. I mean, perhaps it is possible with some Proxy or other JS trickery, but by default JavaScript arrays don't behave in that way. You just create an array with values initialized beforehand and that's it.

And when saying that an array has a length, we in fact mean that the array is of a finite length. There are no infinite arrays in JavaScript.

Those two qualities point to the eagerness of arrays.

On the other hand, iterators are lazy.

To show that, we will create two custom iterators - the first one will be an infinite iterator, in contrast to finite arrays, and the second will initialize its values only when they are actually needed/requested by whoever is using the iterator.

Let's start with the infinite iterator. This might sound scary, but we will create something very simple - an iterator that starts at 0 and at each step returns the next integer in a sequence. Forever.


const counterIterator = {
    integer: -1,

    next() {
        this.integer++;
        return { value: this.integer, done: false };
    },

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

Enter fullscreen mode Exit fullscreen mode

That's it! We start with the integer property equal to -1. At each next call we bump it by one and return it as a value in the result object.

Note that we used here the same trick as before - starting at -1 in order to return 0 as the first result.

Look also at the done property. It will be always false. This iterator never ends!

Third thing, which you've probably noticed yourself - we have made this iterator an iterable, by giving it a simple [Symbol.iterator]() implementation.

And one last note. This is the case that we've been mentioning earlier - we've created an iterator, but there is no iterable in sight! This is an iterator which doesn't need an iterable "parent" for anything.

We can now try out this iterator in a for ... of loop. We just need to remember to break out of the loop at some point. Otherwise, the code would run forever!

for (let element of counterIterator) {
    if (element > 5) {
        break;
    }

    console.log(element);
}
Enter fullscreen mode Exit fullscreen mode

After running this code we will see the following in the console:

0
1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

So we really created an infinite iterator, which can return you as many integers as you wish. And it was actually very easy do achieve!

Now, let's make an iterator, which doesn't create its values until they are requested.

Well... we already did it!

Have you noticed that at any given moment, our counterIterator stores only one number on the integer property? It stores only the last number that it has returned in a next call.

This is indeed the laziness we were talking about. This iterator can potentially return any number (non-negative integer, to be specific). But it only creates a number when it is actually needed - when someone is calling the next method.

This might not seem like a big benefit. After all, numbers are created fast and they don't occupy a lot of memory.

But if you are dealing with very big, memory-heavy objects in your code, sometimes swapping arrays for iterators can be extremely valuable, making your program faster more memory efficient.

The heavier the object (or the longer it takes to create it), the bigger the benefit.

Some other ways to consume iterables

So far we've been playing only with a for ... of loop, or we've been consuming our iterators manually, using the next method.

But those are not your only options!

We've already seen that Map constructor accepts an iterable as an argument.

You can also easily transform an iterable into an actual array by using Array.from method. Be careful though! As we've said, laziness is sometimes a big benefit of an iterator. Converting it to an array gets rid of all the laziness. All the values returned by an iterator get initialized immediately and then they get put into an array.

In particular, this means that trying to convert our infinite counterIterator into an array would result in a catastrophe. Array.from would just run forever and never return any result! So before converting an iterable/iterator to an array, make sure it's a safe operation.

Interestingly, iterables also play nicely with a spread operator (...). Just keep in mind that this works similarly to an Array.from, where all the values of an iterator get initialized at once.

For example, we can use the spread operator to create our own version of Array.from.

We just apply the operator on an iterable and then put the values into an array:


const arrayFromIterator = [...iterable];

Enter fullscreen mode Exit fullscreen mode

We can also get all the values from an iterable and apply them to a function:


someFunction(...iterable);

Enter fullscreen mode Exit fullscreen mode

Conclusion

I hope that at this point you understand why the title of this article was "Iterables and Iterators".

We've learned what are they, how do they differ, how to use them, and how to create your own instances of them.

This makes us more than ready to deal with generators. In fact, if you understand iterators well, then jumping into generators should be no problem at all!

If you've enjoyed this article and want more, remember to subscribe to me on Twitter.

Thanks for reading!

Top comments (0)