Iterators, Iterables, and Generators

Published: Monday, October 19, 2020
Updated: Thursday, October 29, 2020

Greetings, friends! Have you ever been confused between the difference between iterators, iterables, and generators in the world of JavaScript? I know when I was learning JavaScript, I would constantly get confused by iterators and iterables, and I still get them mixed up today 😅. Let's learn about each of these topics and how they relate to each other.

Iterators

As stated on this MDN article about iterators, a JavaScript iterator is an object that defines a sequence and may or may not return a value upon its termination or completion. Specifically, an iterator is a JavaScript object that implements the Iterator protocol/interface. What does this mean? If you're familiar with TypeScript, it essentially means this:

typescript
Copied! ⭐️
interface Iterator {
  next() : IteratorResult;
}
interface IteratorResult {
  value: any;
  done: boolean;
}

If you're not familiar with TypeScript, no worries! I'll explain it another way. For an object to be considered an iterator, it needs to have a next method that returns an object with the keys, value and done. Let's see a vanilla JavaScript example of this:

js
Copied! ⭐️
const myCoolIterator = {
  next: function() {
      return { value: 1, done: true }
  }
};

According to the method definitions page, you could have also used this shorthand notation instead:

js
Copied! ⭐️
const myCoolIterator = {
  next() {
      return { value: 1, done: true }
  }
};

That's all it takes for an object to become an iterator! However, the above example is rather boring 😴. If you call the next method on this object, it'll return the same thing every time. We want our iterator to be useful and thus should iterate through different values.

Let's look at a better example, found on MDN.

js
Copied! ⭐️
// This function returns an iterator object
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;

  const rangeIterator = {
    next: function() {
      let result;
      if (nextIndex < end) {
          result = { value: nextIndex, done: false }
          nextIndex += step;
          iterationCount++;
          return result;
      }
      return { value: iterationCount, done: true }
    }
  };
  return rangeIterator;
}

This function can be thought of as an iterator factory. When we call the makeRangeIterator function, it will create a new iterator object. Let's use this function now:

js
Copied! ⭐️
const it = makeRangeIterator(1, 10, 2);

console.log(it.next()); // { value: 1, done: false}
console.log(it.next()); // { value: 3, done: false}
console.log(it.next()); // { value: 4, done: false}
console.log(it.next()); // { value: 5, done: false}
console.log(it.next()); // { value: 9, done: false}
console.log(it.next()); // { value: 5, done: true}
console.log(it.next()); // { value: 5, done: true}
console.log(it.next()); // { value: 5, done: true}

What's happening here? Every time we call the next function, we're iterating the value by 2. Eventually, we hit the number 11, and it's greater than the end value of 10, so we start returning a new object with done set to true. Now, I know what you're thinking. What's so special about this? This is just a normal function, right? We could have changed the method name, next, to stuff, and the function would still behave normally. To see why iterators are useful and why we need to adhere to the iterator protocol, let's first learn about iterables.

Iterables

Iterables are objects that implements the iterable protocol/interface. If you're familiar with TypeScript, then this is similar to the following:

typescript
Copied! ⭐️
interface Iterable {
  [Symbol.iterator]() : Iterator;
}

What does this mean? It's saying that for an object to be an iterable, it must contain a method named Symbol.iterator that returns an iterator object. Yes, the names can get quite confusing indeed. Iterators...iterables...vegetables...yikes 😵. So an iterable is a JavaScript object that has a method named Symbol.iterator that returns a function named next that then returns an object with the keys, value and done. Yes, that sounds confusing. Let's look at an example:

js
Copied! ⭐️
const myCoolIterable = {
  [Symbol.iterator]: function() {
    return {
      next: function() {
        return { value: 0, done: true }
      }
    }
  }
}

Again, you can use the shorthand notation for object methods:

js
Copied! ⭐️
const myCoolIterable = {
  [Symbol.iterator]() {
    return {
      next() {
        return { value: 0, done: true }
      }
    }
  }
}

Let's check to see if our iterable works:

js
Copied! ⭐️
console.log(myCoolIterable[Symbol.iterator]().next());
// Output: { value: 0, done: true }
tip
If you're confused about the square brackets around the property name, "Symbol.iterator," this is known as a computed property name. This lets us use dots/periods inside the property name. Additionally, we can insert variables or evaluate expressions to generate dynamic property names.

The previous examples weren't that useful, since the next method returns the same thing, so let's make a more interesting example. Suppose we had the following iterable:

javascript
Copied! ⭐️
const iterable = {
  [Symbol.iterator]() {
    return {
      i: 0,
      next() {
        this.i = this.i + 2;
        if (this.i < 11) {
          return { value: this.i, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

Instead of calling the next method over and over, let's use a for...of loop loop to iterate through the iterable instead:

javascript
Copied! ⭐️
for (const value of iterable) {
  console.log(value);
}

/* 
Output:
2
4
6
8
10
*/

Wait, what? How is that possible? 😮 What wizardry is this? 🧙 It turns out that the for...of loop iterates through objects that implement the iterable protocol. Custom iterables let us customize the behavior of for...of loops! Pretty cool! By extension, we implement the iterator protocol when we implement the iterable protocol, since our iterable needs to implement a method named Symbol.iterator that returns an iterator. In most cases, you'll usually implement both the iterator and iterable protocols, not just one of them.

tip
You may notice that on MDN, it mentions an @@iterator method. This is identical to the Symbol.iterator method. The @@ symbols are used to denote common symbols in the official ECMAScript specification.

Let's dive deeper into understanding what the for...of loop does internally. The for...of loop will first call the Symbol.iterator method of the iterable passed into it which will then return an object that contains the next method. Upon each iteration of the for...of loop, it will then call the next method. Keep this in mind if you wish to do some setup logic inside the Symbol.iterator method definition.

javascript
Copied! ⭐️
const iterable = {
  [Symbol.iterator]() {
    const initialValue = 0;
    console.log('Symbol.iterator method called');
    return {
      i: initialValue,
      next() {
        console.log('next method called');
        this.i = this.i + 2;
        if (this.i < 11) {
          return { value: this.i, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

for (const value of iterable) {
  console.log(value);
}

/* OUTPUT:
Symbol.iterator method called
next method called
2
next method called
4
next method called
6
next method called
8
next method called
10
*/

If you break the loop immediately, you'll still see the Symbol.iterator method get invoked by the for...of loop. You'll also see the next method get invoked only one time.

javascript
Copied! ⭐️
const iterable = {
  [Symbol.iterator]() {
    const initialValue = 0;
    console.log('Symbol.iterator method called');
    return {
      i: initialValue,
      next() {
        console.log('next method called');
        this.i = this.i + 2;
        if (this.i < 11) {
          return { value: this.i, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

for (const value of iterable) {
  break;
  console.log(value); // Won't log any value here since break ends the loop
}

/* OUTPUT:
Symbol.iterator method called
next method called
*/

Iterables are all around us in the JavaScript world. Arrays, Strings, Maps, and Sets are all iterable. Don't believe me? Let's see if they implement a Symbol.iterator method:

javascript
Copied! ⭐️
Array.prototype[Symbol.iterator] === undefined // false
String.prototype[Symbol.iterator] === undefined // false
Map.prototype[Symbol.iterator] === undefined // false
Set.prototype[Symbol.iterator] === undefined // false
Object.prototype[Symbol.iterator] === undefined // true

Looks like the Symbol.iterator method exists on all of them which means they're iterables. This means that they can all be used in a for...of loop! Wait... objects don't seem to be iterable 🤔. Normally, JavaScript objects cannot be iterated over using for...of loops. They don't have default iterable protocols like the other JavaScript data structures. If you run the following in your JavaScript console, you'll get an error:

javascript
Copied! ⭐️
const foods = {pizza: '🍕', pineapple: '🍍'}

for (const food of foods) {
  console.log(food);
}

// Error thrown: Uncaught TypeError: foods is not iterable

However, we've shown that by implementing a Symbol.iterator method on an object that follows the iterable protocol, we can make objects iterable too. JavaScript objects just don't come with a Symbol.iterator method by default and thus constructs like for...of loops don't work on them.

The for...of loops aren't the only constructs that accept an iterable. Let's look at other JavaScript constructs that support iterations over iterables.

Array destructuring:

javascript
Copied! ⭐️
const [pizza, pineapple] = new Set(['🍕', '🍍']);

console.log(pizza, pineapple); // 🍕 🍍

The Array.from() method:

javascript
Copied! ⭐️
const arr = Array.from(new Set(['a', 'b', 'c']));

console.log(arr); // ["a", "b", "c"]

Spread operator:

javascript
Copied! ⭐️
const arr = [...new Set(['a', 'b', 'c'])];

console.log(arr); // ["a", "b", "c"]

Constructors of Maps and Sets:

javascript
Copied! ⭐️
const map = new Map([[false, 'no'], [true, 'yes']]);
const set = new Set(['a', 'b', 'c']);

console.log(map); // Map(2) {false => "no", true => "yes"}
console.log(set); // Set(3) {"a", "b", "c"}

Promise.all() and Promise.race():

javascript
Copied! ⭐️
const wait3Seconds = new Promise(
  (resolve) => setTimeout(() => resolve('3 seconds have passed')),
  3000
);

const wait5Seconds = new Promise(
  (resolve) => setTimeout(() => resolve('5 seconds have passed')),
  5000
);

Promise.all([wait3Seconds, wait5Seconds]).then(console.log); // ["3 seconds have passed", "5 seconds have passed"]

Promise.race([wait3Seconds, wait5Seconds]).then(console.log); // 3 seconds have passed

The yield* keyword:

javascript
Copied! ⭐️
function* delegateToIterable() {
  yield* [1, 2, 3];
}

const iterator = delegateToIterable();

console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

Since Arrays, Strings, Maps, and Sets are all iterables, they can be passed into each of these constructs. As long as you give any of these constructs an object that adheres to the iterable protocol, then they should work correctly. This means we can create our own iterable and use them with each of these constructs. For example, let's use the spread operator on a custom iterable.

javascript
Copied! ⭐️
const iterable = {
  [Symbol.iterator]() {
    return {
      i: 0,
      next() {
        this.i = this.i + 2;
        if (this.i < 11) {
          return { value: this.i, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

console.log([...iterable]); // [2, 4, 6, 8, 10]

Generators

Now that we have learned about iterators and iterables, what is a generator? Generators lets us pause execution of a function and then call it in a lazy manner. That is to say that we can use the next method to iterate through values specified in the generator function with the yield keyword. Think of the yield keyword as a return statement for generator functions (though they're not quite the same). Every time we call the next method, the function will pick up from where it left off, right after the last yield statement, and then continue execution until another yield statement is hit.

Let's look at a simple example of a generator function that returns a generator:

js
Copied! ⭐️
function *simpleGenerator() {
  // First next statement begins here
  yield 1;

  // Second next statement begins here
  yield 2;

  // Third next statement begins here
  yield 3;

  // Fourth next statement begins here, but there's no more yield statements,
  // so a value of undefined is returned and the done property is set to true
}

// Use the generator
const myGenerator = simpleGenerator();

console.log(myGenerator.next()) // { value: 1, done: false }
console.log(myGenerator.next()) // { value: 2, done: false }
console.log(myGenerator.next()) // { value: 3, done: false }
console.log(myGenerator.next()) // { value: undefined, done: true }

Notice the asterisk, *, before simpleGenerator. By placing an asterisk before the function name, it allows us to create a generator function instead of a normal function.

You can also move the asterisk to the right of the function keyword, and it still creates a generator function:

js
Copied! ⭐️
function* simpleGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

When we run the next method, it will run through the simpleGenerator function until it hits the first yield statement. Then, when we run next again, it will pick off right after the first yield statement and before the second yield statement. This pattern continues until there are no more yield statements left, or the function is escaped through another means such as a break statement or error occurring. Generators help us create iterators that automatically sets the done property to true once it has finished completing. Generally, you'll create generators a lot more often than creating iterators from scratch.

So is a generator an iterator or an iterable? It's actually both! It adheres to both the iterator and iterable protocols. A generator can be thought of as a JavaScript object that implements both a next method and a Symbol.iterator method. They provide a simpler syntax for creating and using iterators.

One big distinction between generators and custom iterables is that generators can only iterate through values once. Suppose we used a generator to create an iterator:

javascript
Copied! ⭐️
function* makeIterator() {
  yield 1;
  yield 2;
}

const it = makeIterator();

Since our generator is an iterable, we can iterate through it using a for...of loop, but if we try to do that multiple times, we'll no longer receive any more values from the generator:

javascript
Copied! ⭐️
for (const value of it) {
  console.log(value);
}

/* Output:
1
2
*/

for (const value of it) {
  console.log(value);
}

// Nothing will be output, since the generator can only be iterated once.

According to MDN, iterables which can iterate only once (such as Generators) customarily return the this value from their Symbol.iterator method. We can prove this is true through the following statement (based on the generator we created in our above example):

javascript
Copied! ⭐️
console.log(it[Symbol.iterator]() === it) // true

If we want our generator to return multiple times, we can overwrite the Symbol.iterator such that it yields values, but doesn't return this:

javascript
Copied! ⭐️
function* makeIterator() {
  yield 1;
  yield 2;
}

const it = makeIterator();

it[Symbol.iterator] = function* () {
  yield 2;
  yield 1;
};

for (const value of it) {
  console.log(value);
}

/* Output:
2
1
*/

for (const value of it) {
  console.log(value);
}

/* Output:
2
1
*/

Now the generator can be iterated over multiple times using constructs such as for...of loops.

A generator essentially follows a pattern similar to the following:

js
Copied! ⭐️
const generator = {
  next() {
    // logic that returns an object with value and done properties
  },
  [Symbol.iterator]: function() { return this; }
};

For example, let's create a function that behaves like a generator function:

js
Copied! ⭐️
function makeGenerator() {
  let count = 0;

  const generator = {
    next() {
      if (count < 3) return { value: count++, done: false }
      else return { value: undefined, done: true }
    },
    [Symbol.iterator]: function() { return this; }
  };
  return generator;
}

// Use the generator
const myGenerator = makeGenerator();

console.log(myGenerator.next()) // { value: 0, done: false }
console.log(myGenerator.next()) // { value: 1, done: false }
console.log(myGenerator.next()) // { value: 2, done: false }
console.log(myGenerator.next()) // { value: undefined, done: true }

The above example is approximately equivalent to using a real generator function:

js
Copied! ⭐️
function *makeGenerator() {
  let count = 0;
  while (count < 3) {
      yield count++;
  }
}

// Use the generator
const myGenerator = makeGenerator();

console.log(myGenerator.next()) // { value: 0, done: false }
console.log(myGenerator.next()) // { value: 1, done: false }
console.log(myGenerator.next()) // { value: 2, done: false }
console.log(myGenerator.next()) // { value: undefined, done: true }

By returning this in our Symbol.iterator method, we are making the iterable be able to iterate through values only once like a generator. That is why these examples are approximately considered equivalent. Their behaviors are similar.

It's important to note that you can also create generator functions inside objects:

javascript
Copied! ⭐️
const myCoolObject = {
  *generator() {
    yield 1;
    yield 2;
    yield 3;
  }
}

const myGenerator = myCoolObject.generator();

console.log(myGenerator.next()) // { value: 1, done: false }
console.log(myGenerator.next()) // { value: 2, done: false }
console.log(myGenerator.next()) // { value: 3, done: false }
console.log(myGenerator.next()) // { value: undefined, done: true }

This syntax is similar to creating generator functions inside classes:

javascript
Copied! ⭐️
class myCoolClass {
  *generator() {
    yield 1;
    yield 2;
    yield 3;
  }
}

const classInstance = new myCoolClass();
const myGenerator = classInstance.generator();

console.log(myGenerator.next()) // { value: 1, done: false }
console.log(myGenerator.next()) // { value: 2, done: false }
console.log(myGenerator.next()) // { value: 3, done: false }
console.log(myGenerator.next()) // { value: undefined, done: true }

We can also create custom iterables using generators, since a generator is both an iterator and iterable. Therefore, we can assign the System.iterator method to a generator function:

javascript
Copied! ⭐️
const myIterable = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
}

for (let value of myIterable) {
  console.log(value); 
}
/*
Output:
1
2
3
*/

// Spread operator
console.log([...myIterable]); // [1, 2, 3]

If you want to learn more about generators, then check out my next article where I cover generators in depth and discuss how to avoid common pitfalls.

Conclusion

How often are iterators, iterables, and generators used in the JavaScript world? Many developers may not even need to touch them. Even in my own career, it's rare to make custom iterators and iterables. From my personal experience, I would say generators are used more often. Some libraries such as Redux-Saga utilize generators extensively for handling asynchronous state management in Redux applications, commonly paired with React.

Generators and iterables let us accomplish lazy evaluation. Most programming languages have some method for lazily evaluating functions. This may help save memory and/or time to improve performance in your application. Hope to you see you generate some amazing code! Till next time, happy coding!

Resources