Post cover

A Simple Explanation of JavaScript Iterators

A collection is a data structure that contains elements. For example, a string is a collection of characters and an array is a collection of ordered items:


const message = 'Hi!'; // consists of 'H', 'i' and '!'
const numbers = [1, 3, 4]; // consists of 1, 3 and 4

To easily access elements of collections of different structure, JavaScript implements a special pattern named iterator.

In this post, as a part of the iterator pattern, you'll learn what are iterables and iterators. You'll also learn about iterables consumers: how to iterate over a collection using for...of cycle, transform any iterable to an array using the spread operator [...iterable], and more.

1. Digging up the iterable

I don't want to jump right into the dry theory of iterators. I know how confusing they are. On the contrary, let's start with a warm-up example to dig up the concept of iterable.

Let's reuse the numbers array from the introduction. Your task is to simply log to console each item of this array:


const numbers = [1, 3, 4];
for (const item of numbers) {
console.log(item);
}
// logs 1, 3, 4

As expected, the for...of loop logs to console each item of numbers array. Good.

Now let's try another experiment. Can for...of enumerate each property of a plain JavaScript object?


const person = { name: 'Eric', address: 'South Park' };
for (const prop of person) {
console.log(prop);
}
// Throws "TypeError: person is not iterable"

Not this time. for...of cycle cannot iterate over the properties of person object. Why does it happen?

You can find the answer in the error message: TypeError: person is not iterable. The for...of cycle requires an iterable collection to iterate over its items.

So, the first rule of thumb whether a data structure is iterable is try to iterate it using for...of.

Having this warm-up experiment, let's state stricter what an iterable is in the next section.

2. Iterable and iterator interfaces

An object is Iterable when it conforms to Iterable interface.

The Iterable interface requires the object to contain a method Symbol.iterator that must return an Iterator object.


interface Iterable {
[Symbol.iterator]() {
//...
return Iterator;
}
}

In simple words, any object is iterable (iter + able meaning able to be iterated) if it contains a method name Symbol.iterator (symbols can also define methods) that returns an Iterator.

Ok. But what's an Iterator? Let's find out:

The Iterator object must conform to Iterator interface.

The Iterator object must have a method next() that returns an object with properties done (a boolean indicating the end of iteration) and value (the item extracted from the collection at the iteration).


interface Iterator {
next() {
//...
return {
value: <value>,
done: <boolean>
};
};
}

I know that these theoretical terms are confusing. But stay with me.

2.1 How array conforms to iterable

You know from the warm-up experiment that the array is iterable. But how does exactly the array conform to the Iterable interface?


const numbers = [1, 3, 4];
numbers[Symbol.iterator](); // => object

Invoking the expression numbers[Symbol.iterator]() shows that the array instance contains the special method Symbol.iterator. This makes the array conform to the Iterable interface.

The numbers[Symbol.iterator]() method must return the iterator object.

The iterator object is the one that performs the iteration over the array items. Just call iterator.next() to access each item of the array!


const numbers = [1, 3, 4];
const iterator = numbers[Symbol.iterator]();
iterator.next(); // => { value: 1, done: false }
iterator.next(); // => { value: 2, done: false }
iterator.next(); // => { value: 3, done: false }
iterator.next(); // => { value: undefined, done: true }

Each invocation of iterator.next() returns an object { value: <item>, done: <boolean> }.

The value property contains the iterated item, while done indicates whether the iteration is complete.

When there are no more items to iterate, iterator.next() returns { value: undefined, done: true }.

3. Consumers of iterables

JavaScript provides a good set of cycles, syntaxes, and functions that consume iterables.

3.1 for...of cycle

As you know already, for...of cycle accepts an iterable object and iterates through its items:


const message = 'Hi!';
for (const char of message) {
console.log(char);
}
// logs 'H', 'i', '!'

In the above example, message is a string type that is an iterable. for...of cycle iterates over the characters in the string.

3.2 Spread operator

Another great consumer of iterables is the spread operator [...iterable]:


const message = 'Hi!';
const chars = [...message];
chars; // => ['H', 'i', '!']

The spread operator [...message] iterates over the characters of the string and creates an array of these characters.

3.3 Array destructuring

The array destructuring syntax can destructure iterables too!


const message = 'Hi!';
const [firstChar, ...restChars] = message;
firstChar; // => 'H'
restChars; // => ['i', '!']

[firstChar, restChars] = message is a destructuring assignment that destructures the iterable string message.

firstChar is assigned with the first character 'H'. The rest of the characters ['i', '!'] are stored into the array restChars.

3.4 Array.from()

Array.from(iterable[, mapFunction]) also accepts an iterable and transforms it into an array.


const message = 'Hi!';
const chars = Array.from(message);
chars; // => ['H', 'i', '!']

4. Native iterable types

Many native data types in JavaScript are iterables. What makes the iterator pattern in JavaScript so flexible and useful is that any iterable can be consumed by any iterable consumer.

Here's a list of popular iterable data types.

4.1 Iterable array

The array is iterable over its items:


const numbers = [1, 3, 4];
for (const item of numbers) {
console.log(item);
}
// logs 1, 3, 4

4.2 Iterable string

The string primitive is iterable over the characters:


const message = 'Hi!';
for (const char of message) {
console.log(char);
}
// logs 'H', 'i', '!'

4.3 Iterable Map

The Map object is iterable over its key and value pairs.


const map = new Map();
map.set('name', 'Eric');
map.set('address', 'South Park');
for (const [key, value] of map) {
console.log(key, value);
}
// logs 'name', 'Eric'
// logs 'address', 'South Park'

4.4 Iterable Set

The Set object is iterable over its items:


const set = new Set(['blue', 'red', 'green']);
for (const item of set) {
console.log(item);
}
// logs 'blue', 'red', 'green'

5. Summary

Iterables are collections that can be iterated. To be an iterable, the object must conform to Iterable interface.

Iterable consumers are language constructs that consume iterables. for...of cycle is an iterable consumer that cycles over each item of the iterable, spread operator [...iterable] create an array from the iterable's items.

What makes the iterator pattern so useful is that any iterable can be used by any iterable consumer. Even more: you can define your own iterable types, and even define your own iterable consumers!

Challenge: how would you implement an iterable that generates n random numbers?

Like the post? Please share!

Dmitri Pavlutin

About Dmitri Pavlutin

Software developer and sometimes writer. My daily routine consists of (but not limited to) drinking coffee, coding, writing, overcoming boredom 😉. Living in the sunny Barcelona. 🇪🇸