JavaScript: Iterators & Generators

Iterate through each element of a “set of elements” is a very normal operation. JavaScript provides us with a lot of methods to do that.

Itchishiki Satoshi
May 12 · 8 min read

However, in this article, we will not fall in love with those methods. Instead, we will explore the mechanism behind it all, which are the concepts: iterable, iterator, generator.

Photo by Caspar Camille Rubin on Unsplash

Iterable

An object is called iterable if it has the installed iterable protocol (talk later). These iterable objects can often use element browsing methods, for example .

In JavaScript, is iterable.

for (let v of [1, 2, 3, 4]) console.log(v)
// 1
// 2
// 3
// 4

In addition, many other built-in objects are iterable, such as :

for (let v of 'hello') console.log(v)
// h
// e
// l
// l
// o

If an object represents a “set of elements”, then we can completely use for..of to loop through its elements. The operation of browsing through these elements is called “iteration” (similar to many other languages).

But have you ever wondered how things like that work? No need to wait long, we will find out shortly.

Iterable protocol

The Iterable protocol is a protocol that allows JavaScript objects to customize their own browsing operations, which will be used in element approvals (for example, ).

Many of JavaScript’s built-in data types already have iterable protocols installed (for example, , ) that allow us to easily loop through its elements.

An object can only be iterable if it implements the iterable protocol. The installation will be done via the method. That is, the object must have the attribute. Setting this property can be done via as follows:

let range = {
from: 1,
to: 5,
[Symbol.iterator]: () => {
return {
current: this.from,
last: this.to,
next: () => {
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}

The property (set via ) is a function with no parameters, and its result must be an iterator (will learn later) before we can browse the elements. of an object is. Actually, this function can return anything, but if it’s not an iterator then we will get an error as soon as we browse the element, and I think no one does this “stupid” thing when setting the property for a whole object.

Whenever an object needs to be iterated through its elements, for example using . Those methods first call the of that object (without parameter), then use the result of this method to return the iteration.
We can completely build our own iterable objects, by defining the method ourselves in the above example. By self-defining methods, we can completely customize the method of element iterating.

As in the example above, we will browse elements 1–5 (actually, these “elements” are not entirely within the object, but thanks to customizing the browse method, we have absolutely iterate through these elements).

for(let num of range) console.log(num)
// 1
// 2
// 3
// 4
// 5

Browsing methods like for..of only work with iterators that are returned. Note that this iterator may be a completely different object, not an approved object, depending on the settings of each specific object.

Iterator

In JavaScript, an iterator is an object that defines a “string” of elements and will return each of those elements, until a certain endpoint is encountered. For a more precise definition, iterators are objects implemented the iterator protocol.

The Iterator protocol is entirely independent of the iterable protocol, and an object does not necessarily have to be installed on both protocols. That also means that an iterable object is not necessarily an iterator and vice versa.

Iterator protocol
The Iterator protocol is a protocol that defines the standard method to “generate” a sequence of results (which may be finite or infinite), and return the results in turn for us to use in the iteration.

This is the protocol that will be used with for iterators returned from the attribute.

An object will be an iterator, if it defines the iterator protocol. This protocol is straightforward, it requires that the object must be installed with the next method (no parameters) with the result that returning an object has the following two properties:

  • : indicates whether the iterator has finished or not. It is when there are still other values to browse. In case this value is then the next attribute is required.
  • : the return value of the next browse. The value of this attribute can be any JavaScript object.

The method must always return an object with the above two properties (may not be needed if is ). If an object other than the one described above is returned, we will get a error when performing element browsing.

In fact, it is difficult to know if an object has an iterator protocol installed. But we can quite easily build an object with this protocol installed.

As mentioned above, an iterator object is not necessarily iterable and vice versa, but if we intentionally build an object, we can install both the iterable protocol and the iterator protocol. Then, our object will be both an iterable object and an iterator.

Build an iterator yourself
One of JavaScript’s built-in iterators, Array is a prime example. This iterator simply returns each value of the elements in it and we will browse each of them accordingly.

We can imagine that iterators are similar to Arrays. But it’s not like that, in fact. Iterator can completely operate freely (even without a stop), depending on how to install its own iterator protocol.

Below is an example of a self-built iterator. It constructs a sequence of values in a given range.

We can even combine the iterator protocol and the iterable protocol, so we can both construct an iterator, and we can iterate its elements with .

var myIterator = {
next: function() {
// ...
},
[Symbol.iterator]: function() { return this }
};

Generator

Building your own iterator is a very cool and effective job. But it requires that we take great care in setting up the iterable protocol as well as the iterator protocol. Because we absolutely have to do things like manually managing the current state as well as calculating the next value.

But JavaScript has another tool, the generator functions, that will help us be much more relaxed in this. The generator has the ability to help us define a sequence of values to browse, using a single function.

Syntactically, generator functions are defined by the keyword , which is a bit different from regular functions. Generator performance is also very different. When called, it is not actually executed immediately. Instead, it returns an object, which we can call a generator.

Generator is iterator

A generator will automatically be defined on the method, so it will automatically be an iterator. Even this is fully automatic so we do not need to worry about managing the status and values of the generator.

Every time the method is , the generator function at this time is actually executed, and it will stop until it meets . And the value of is also the value that will be returned by .

Going back to the example of the self-built iterator above, we can easily rewrite it with the generator as follows:

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
let iterationCount = 0;
for (let i = start; i < end; i += step) {
iterationCount++;
yield i;
}
}

With the predefined method, we can call the following:

range = makeRangeIterator(1, 10, 2);
range.next()
// {value: 1, done: false}
range.next()
// {value: 1, done: false}

Generator is iterable
Not only is an iterator, the generator is also an iterable object. That is, it will automatically be defined both iterable protocol and iterator protocol, which is convenient for us. Thanks to the protocol implementation, we can use element approvals, similar to other regular iterables:

for (let num in makeRangeIterator(1, 10, 2)) console.log(num)
// 1
// 3
// 5
// 7
// 9

A generator function can be called as many times as you like. And each call will return a different generator, which can be iterated separately without affecting other calls.

However, unlike the self-built iterator, we cannot customize generator traversing. Each generator can only be browsed once, and each value can only be browsed once. Of course, we can call the generator function multiple times, but each call can only be browsed once.

Thanks to our advantages, we can think of using generators instead of iterable iterators that need to be built manually. Because obviously, using the generator is much more convenient and safer.

Generator yield another generator
This is a special method of the generator, we can “embed” this generator in another generator. Then, we need a special syntax: :

function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {

// 0..9
yield* generateSequence(48, 57);

// A..Z
yield* generateSequence(65, 90);

// a..z
yield* generateSequence(97, 122);

}

let str = '';

for(let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}
console.log(str):
// 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

The syntax above allows us to call another generator, and wait for that generator to finish before performing the next calculation. Without using this method, we have to build a more complex generator as follows:

function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}

function* generateAlphaNum() {

// yield* generateSequence(48, 57);
for (let i = 48; i <= 57; i++) yield i;

// yield* generateSequence(65, 90);
for (let i = 65; i <= 90; i++) yield i;

// yield* generateSequence(97, 122);
for (let i = 97; i <= 122; i++) yield i;

}

let str = '';

for(let code of generateAlphaNum()) {
str += String.fromCharCode(code);
}
console.log(str);
// 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

The yield of another generator gives us a much simpler method of embedding a generator into a generator.

Assign values to yield

From the beginning, we still use the generator in a very natural way, which in turn each value of the generator. Many are doing the same. But the generator is very flexible, allowing us to do more than that.

That yield can also recognize the value from the outside, not the mound in returning the value. Consider a simple example below, we use this trick to reset the string from the beginning:

function* foo() {
let index = 0;
while (true) {
const result = yield index++;
if (result) {
index = 0;
}
}
}

const bar = foo();

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

Generator also has one very large application, which is the basis for asynchronous code with keywords.

Conclude

Iterators and generators are very important concepts in JavaScript. Hopefully the article provides useful information and helps you become better in JavaScript code. 👏

JavaScript In Plain English

New articles every day.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store