What the Heck Are Iterators and Generators in JavaScript

What the Heck Are Iterators and Generators in JavaScript

Have you ever come across syntax like function* or statements such as it.next() and didn't have a clue what these are, where they come from and what they can be used for?

You've come to the right place! Recently I needed to revisit iterators and generators in JavaScript. Here's a simple introduction to what these are and what are their applications.

Iterator

Let's first look at what iterator is from a syntax perspective. Simply put, an iterator is a JavaScript object that implements a zero-argument next method which returns an object with at least two properties - done and value. It's called the Iterator Protocol.

Let's look at a simple example of an iterator.

const iterator = {
    next() {
        return {
            value: "Hello, I'm an iterator",
            done: true,
        };
    },
};

Nothing crazy. As you might expect, having defined this, we can do the following.

const result = iterator.next();
console.log(result.value);
console.log(result.done);
Hello, I'm an iterator
true

And that's an iterator. Now we know what it is, let's look at what it can be used for. Documentation on Iterators and Generators mentions the following.

In JavaScript an iterator is an object which defines a sequence and potentially a return value upon its termination.

Instead of having an iterator that always returns the same value, let's make an iterator that will allow iterating over a sequence of words. How about Hello World for a starter?

const words = ["Hello", "World"];
let index = 0;

const iterator = {
    next() {
        return {
            value: words[index++],
            done: index === words.length + 1,
        };
    },
};

We can run this in a similar way.

let result = iterator.next();
console.log(result.value, result.done);

result = iterator.next();
console.log(result.value, result.done);

result = iterator.next();
console.log(result.value, result.done);
Hello false
World false
undefined true

Cool, but this is overly imperative, this could have been done with a simple loop with less code. What can be done about this then? To understand this, we need to look at another construct that builds on top of an iterator - Iterable Protocol.

Iterable

Iterable is an object which can specify a custom looping behaviour by exposing a method under a special Symbol.iterator key which returns an iterator. And guess what uses the Iterable protocol? Loops - for .. of loop in particular.

const words = ["Hello", "World"];
let index = 0;

const iterator = {
    next() {
        return {
            value: words[index++],
            done: index === words.length + 1,
        };
    },
};

const iterable = {
    [Symbol.iterator]() {
        return iterator;
    },
};

Having this defined we can do the following.

for (let word of iterable) {
    console.log(word);
}
Hello
World

This is an example of an iterable defining the sequence of values and the for .. of loop being the algorithm that iterates over these values. But you can think about implementing your own mechanism that based on these protocols will allow iterating in more intricate ways, depending on the requirements that you might have.

Generators

When you look closely at Iterator and Iterable protocols you can spot that these are not mutually exclusive. An object could technically satisfy both! It might look something like this.

const iteratorAndIterable = {
    next() {
        // ...
    },
    [Symbol.iterator]() {
        return this;
    },
};

There's a name for such an object - a generator. On its own, it might not be really useful. What is important to know, though, is that it is returned from a generator function - function declared with function* syntax. This type of function allows us to define a sequence in a different, somewhat streamlined way, using the yield keyword. Let's extend our Hello World example, adding a user name at the end. Instead of defining an iterable, let's use a generator function.

function* generateHelloWorld(name) {
    yield "Hello";
    yield "World";
    yield name;
}

What this returns is a generator. It will return whatever the function yields to on each subsequent call of its next function. But you already know that we don't have to call next manually, we can simply pass such generator to a for .. of loop (since a generator is an iterable, as mentioned before). Let's do this!

const generator = generateHelloWorld("Tomasz");

for (let word of generator) {
    console.log(word);
}
Hello
World
Tomasz

Awesome! We achieved even more flexibility with less code.

Conclusion

That's it! Iterators and iterables provide interfaces we can use to define sequences or values. These sequences can be processed by custom algorithms or by inbuilt JavaScript constructs such as for .. of loop. The generator function provides an easy way to create iterators.

I hope this allows you to grasp what iterators and generators are and what they can be useful for!

Further reading and references