This page looks best with JavaScript enabled

Iterators and generators work great together

 ·  ☕ 6 min read  ·  ✍️ Iskander Samatov

generators



Iterators and generators are both interesting JavaScript features. And even more so when you use them together. In this blog post, let’s brush up on our understanding of the generators and iterators, and see how we can combine them to write elegant JavaScript code.

Iterator

Put simply, iterator is a symbol that you can use to make any object iterable. Iterable objects can be iterated using for…of loop. Firstly, let’s go over what the symbols are.

Symbol

Symbol is a type of primitive that is guaranteed to be unique. One of its primary purposes is to be used as key for dictionary collections (maps or plain objects). Also, the fact that it’s always unique makes it a good candidate for other interesting use cases. Now let’s explore what I mean by symbol being truly unique:

1
2
3
4
5
6
7
8
9
const symbol = Symbol('Unique');
 const notSymbol = 'Unique';
 if (notSymbol !== symbol) {
     console.log(`<code>String ${notSymbol} is not equal to ${symbol.toString()}</code>`);
 }
 const symbol2 = Symbol('Unique');
 if (symbol !== symbol2) {
     console.log(`<code>${symbol.toString()} is not equal to ${symbol2.toString()}</code>`);
 }

The above code will print:

String Unique is not equal to Symbol(Unique)
Symbol(Unique) is not equal to Symbol(Unique)

First if compares the stringnotSymbol with a value of Unique to a symbol instance with the description value of Unique. As you can see, these two variables are not equal. A more interesting case is the second if, where we’re comparing two symbols that might seem identical at first. However, even though both symbols have the same description value, they are not equal. Description is used solely to improve the readability of our code.

Defining iterators and iterables

JavaScript providesSymbol.iterator static symbol that we can use to make any object iterable. Let’s take a look at this example:

First let’s define a class that we’ll be iterating over:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

class PetShelter {
    constructor() {
        this.pets = [
            { type: 'cat', name: 'Emma' },
            { type: 'dog', name: 'Bubbles' },
            { type: 'dog', name: 'Hank' },
            { type: 'cat', name: 'Leo' },
        ]
        this[Symbol.iterator] = function() {
            return new PetIterator(this);
        }
    }
}

PetShelter is a simple class that contains an array of objects called pets. Below our pets property, we add another property using Symbol.iterator as a key. We assign a function to it which returns a new instance of the PetIterator.

Now let’s see what the definition of PetIterator looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class PetIterator {
    constructor(shelter) {
        this.shelter = shelter;
        this.counter = 0;
    }


    next() {
        if (this.counter &gt;= this.shelter.pets.length) return { done: true };
        const result = {
            done: false,
            value: this.shelter.pets[this.counter]
        };

        this.counter++;

        return result;
    }
}

PetIterator is just another class that contains only one method – next. Every iterator is required to have the next method. Also, this method must return an object that can contains two properties: done and value. You might’ve guessed what these properties are used for:

  • done is a boolean property that indicates whether or not we’ve reached the end of the iteration.
  • value is used to return a value of the currently iterated item.

Using them together

Now that we have our iterable and iterator, let’s see how we can use them together:

1
2
3
4
5
const shelter = new PetShelter();

for (let pet of shelter) {
    console.log({ pet });
}

Quite simple right? The for...of loop automatically created and consumed a new instance of the iterator provided by the PetShelter class.

The above code will print the following to the console:

{ pet: { type: 'cat', name: 'Emma' } }
{ pet: { type: 'dog', name: 'Bubbles' } }
{ pet: { type: 'dog', name: 'Hank' } }
{ pet: { type: 'cat', name: 'Leo' } }

Generator

Generators are one of the new syntax features introduced with EcmaScript 2015 specification. Just like async, generators can be used to control the execution flow of the program by pausing and resuming it as the client sees fit. Let’s take a look at an example of how we would define a generator:

1
2
3
4
5
function * generatorSample() {
    yield 'Hi'
    yield 'What\'s your name?'
    console.log('I\'m done here');
}

As you can see, generators are just regular functions with a couple of minor differences:

  • In order to define a generator you must put * after the function keyword.
  • yield can be used inside of the generator to pause the execution. Also, any value after the yield keyword is passed back to the client that invokes the generator.

Now let’s see how we can use our generator:

1
2
3
4
5
const genr = generatorSample() // returns a new generator instance

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

Here’s the output of the code above:

Hi
What's your name?
I'm done here

Few things to take a note of here:
  • Calling generatorSample() does not run the generator function, but returns a new instance of the generator instead.
  • Once the code inside of our generator encounters yield, it stops. And we use the next method of the generator to proceed with the execution.

Here’s what the structure of the next object looks like:

1
2
3
4
{
    value: 'Hi' //yielded value,
    done: false //whether the generator is done executing or not
}

Using generators to create iterators

Looks familiar? Hopefully now you’re starting to see a connection between iterators and generators. Frankly, one of the main reason generators were introduced with ES 2015 was to make the process of creating iterators much easier. In fact, all we have to do to turn ourPetShelter into an iterable is make [Symbol.iterator] function return a new instance of a generator. Now let’s see how we can accomplish just that:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const PetIterator = function (shelter) {
    for (let i = 0; i < shelter.pets.length; i++) {
        yield shelter.pets[i];
    }
}


class PetShelter {
    constructor() {
        this.pets = [
            { type: 'cat', name: 'Emma' },
            { type: 'dog', name: 'Bubbles' },
            { type: 'dog', name: 'Hank' },
            { type: 'cat', name: 'Leo' },
        ]
        this[Symbol.iterator] = function () {
            return PetIterator(this);
        }
    }
}

const shelter = new PetShelter();


for (let pet of shelter) {
    console.log({ pet });
}

Just like in the earlier example, the above code will produce the following output:

{ pet: { type: 'cat', name: 'Emma' } }
{ pet: { type: 'dog', name: 'Bubbles' } }
{ pet: { type: 'dog', name: 'Hank' } }
{ pet: { type: 'cat', name: 'Leo' } }

As you can see, we no longer need to define a separate class for our PetIterator. All we had to define was a generator that uses a for loop to iterate over the pets property. Next, we adjusted the [Symbol.iterator] function to return a new instance of the PetIterator generator instead.

We were able to use the generator in place of the iterator because generators already have a method called next that returns an object with done and value properties.

And that’s it for this post! Here we briefly went over the basics of iterators and generators and how you can use them to easily create iterable objects. I hope you found this post useful.

Thank you for reading!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on web and mobile development.