Deep dive into JavaScript's Iterators, Iterables and Generators
The default operations like for...of
loop in JavaScript are powered behind the scene by the concepts of Iterators, Iterables, and Generators. These concepts serve as the protocol for iterating over data structures provided by JavaScript, such as the for...of
loop, used to traverse the elements of specific data structures one by one.
Throughout this article, we’ll discuss and explore these concepts and learn how to implement them within your next JavaScript project.
Symbols for iterators
But before we explain these concepts, we’ll take a short discovery on the JavaScript built-in method, Symbol.iterator()
, which serves the unit block on implementing custom iteration in JavaScript.
// syntax
Symbol.iterator() // Symbol(Symbol.iterator)
The concept of an Iterable is any structure that has the Symbol.iterator
key; the corresponding method has the following behavior:
- When the
for..of
loop begins, it first looks for errors. If none are found, it then accesses the method and the object that the method is defined on. - The object will then be iterated in a
for..of
loop manner. - Then, it uses the
next()
method of that output object to get the next value to be returned. - The values given back will have the format
done:boolean, value: any
. The loop is finished whendone:true
is returned.
let list = {
start: 2,
end: 7,
};
list[Symbol.iterator] = function () {
return {
now: this.start,
end: this.end,
next() {
if (this.now <= this.end) {
return { done: false, value: this.now++ };
} else {
return { done: true };
}
},
};
};
for (let i of list) {
console.log(i);
}
//output
2
3
4
5
6
7
In JavaScript, the Array
object is the most iterative iterable
in nature. Still, any object does possess the nature of a group of the elements—expressing the nature of a range like an integer between 0 and Infinity
, can be turned into an iterable
. And the scope of this article would be to help you understand and implement custom iterables.
Iterator and Iterable in JavaScript
Iteration on any array
of data involves the use of the traditional for
loop to iterate over its elements as below:
let items = [1, 2, 3];
for (let i = 0; i < items.length; i++) {
console.log(items[i]);
}
//output
'1'
'2'
'3'
The above code construct works fine but poses complexity when you nest loops inside loops. This complexity will result from trying to keep track of the multiple variables involved in the loops.
To avoid the mistakes brought on by remembering loop indexes and reduce the loop’s complexity, ECMAScript 2015 developed the for...of
loop construct.
This gives birth to cleaner code, but most importantly, the for...of
loop provides the capacity to iterate/loop over not just an array but any iterable
object.
An iterator
is any object which implements the next()
method that returns an object with two possible key-value pairs:
{ value: any, done: boolean }
Using the earlier discussion on the Symbol.iterator()
, we know how to implement the next()
method that accepts no argument and returns an object which conforms to the above key-value pairs.
With this in mind, we implement a custom iteration process for a custom object type and successfully use the for…of
loop to carry out iteration on the type. The following code block will attempt to create a LeapYear
object that returns a list of leap years in the range of ( start
, end
) with an interval
between subsequent leap years.
class LeapYear {
constructor(start = 2020, end = 2040, interval = 4) {
this.start = start;
this.end = end;
this.interval = interval;
}
[Symbol.iterator]() {
let nextLeapYear = this.start;
return {
next: () => {
if (nextLeapYear <= this.end) {
let result = { value: nextLeapYear, done: false };
nextLeapYear += this.interval;
return result;
}
return { value: undefined, done: true };
},
};
}
}
In the above code, we implemented the Symbol.iterator()
method for the custom type LeapYear
. We have the starting and ending points of our iteration in the this.start
and this.end
fields, respectively. Using the this.interval
, we keep track of the interval between the first element and the next element of our iteration.
Now, we can call the for…of
loop on our custom type and see its behavior and output values like a default array type.
let leapYears = new LeapYear();
for (const leapYear of leapYears) {
console.log(leapYear);
}
// output
2020
2024
2028
2032
2036
2040
As stated earlier, Iterable's are JavaScript structures with the
Symbol.iterator()method, like
Array,
String,
Set. And as a result, our above LeapYear type is an
Iterabletoo. This is a basic idea of
Iterablesand
Iterators`; for more reading, refer here.
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
Generators in Javascript
A generator
is a function that can produce many output values, and may be paused and resumed. In JavaScript, a generator is like a traditional function that produces an iterable Generator
object. This behavior is opposed to traditional JavaScript functions, which execute fully and return a single value with a return
statement. The Generator uses the new keyword, yield
, to pause and return a value from a generator function. For more reading, refer here.
Below is the basic syntax for the declaration of a Generator
:
function* generator() {} // Object [Generator] {}
Let’s explore how generators
and yield
work.
function* generate() {
console.log("invoked first time");
yield "first";
console.log("invoked second time");
yield "second";
}
let gen = generate();
let next_value = gen.next();
console.log(next_value);
// output
// <- invoked first time
// <- {value: 'first', done: false}
next_value = gen.next();
console.log(next_value);
// output
// <- invoked second time
// <- {value: 'second', done: false}
The Generator
function yields an object similar to the Symbol.iterator
two properties: done
and value
fields. In the context of our discussion, this provides that the Generator
type is an Iterable
. This means we can use the for…of
loop on it to iterate through its returned values. The for…of
loop enables us to return the value inside the value
field without the need to return the done
field as the for.of
loop keeps track of the iteration. The for.of
loop is a sugar syntax abstracting the implementation used in the previous code block.
function* generate() {
console.log("invoked first time");
yield "first";
console.log("invoked second time");
yield "second";
}
let gen = generate();
for (let value of gen) {
console.log(value);
}
// output
//invoked first time
//first
//invoked second time
//second
Alternatively, we can also use the concept of Generator
with our LeapYear
type to implement iteration on it. See below code block for the updated code snippet:
class LeapYear {
constructor(start = 2020, end = 2040, interval = 4) {
this.start = start;
this.end = end;
this.interval = interval;
}
*[Symbol.iterator]() {
for (let index = this.start; index <= this.end; index += this.interval) {
yield index;
}
}
}
The above refactored and succinct code will produce the same result as earlier:
// output
2020
2024
2028
2032
2036
2040
From the above output, using the Symbol.iterator
method is much simpler than using the Generator
concept. The above code snippets were used to implement a LeapYear
iterator to generate leap years from the year 2020 to the year 2040.
Generator vs. Async-await — AsyncGenerator
It is possible to simulate the asynchronous behavior of code that “waits,” a pending execution, or code that appears to be synchronous even if it is asynchronous, using both the generator/yield
concepts to imitate the async/await
functions. A generator function’s iterator (the next
method) executes each yield-expression one at a time instead of async-await, which executes each await
sequentially. For async/await
functions, the return value is always a promise that will either resolve to an any
value or throws an error. In contrast, the return value of the generator is always {value: X, done: Boolean}
, and this could make one conclude that one of these functions is built off the other. Understanding that an async function may be broken down into a generator and a promise implementation is helpful. Tada!
Now, combining generator with async/await
gives birth to a new type, AsyncGenerator
. In contrast to a conventional generator, an async generator’s next()
method returns a Promise
. You use the for await...of
construct to iterate over an async generator.
The below code snippet would attempt to asynchronously fetch external data when requested and yield the value.
let user = {
request: 0,
trials: 12,
};
async function* fetchProductButton() {
if (user.trials == user.request) {
return new Error("Exceeded trials");
}
let res = await fetch(`https://dummyjson.com/products/${user.request + 1}`);
let data = await res.json();
yield data;
user.request++;
}
const btn = fetchProductButton();
(async () => {
let product = await btn.next();
console.log(product);
// OR
for await (let product of btn) {
console.log(product);
}
})();
The above code snippet will return the below JSON data:
//output
{
value: {
id: 1,
title: 'iPhone 9',
description: 'An apple mobile which is nothing like apple',
price: 549,
discountPercentage: 12.96,
rating: 4.69,
stock: 94,
brand: 'Apple',
category: 'smartphones',
thumbnail: 'https://dummyjson.com/image/i/products/1/thumbnail.jpg',
images: [
'https://dummyjson.com/image/i/products/1/1.jpg',
'https://dummyjson.com/image/i/products/1/2.jpg',
'https://dummyjson.com/image/i/products/1/3.jpg',
'https://dummyjson.com/image/i/products/1/4.jpg',
'https://dummyjson.com/image/i/products/1/thumbnail.jpg'
]
},
done: false
}
The above code snippet would be a real-life example of simulating session access to always get a value as long as your session access isn’t exhausted. Using the async generator provides flexibility as it could help make paginated requests to an external API or help decouple business logic from the progress reporting framework. Also, using a Mongoose cursor
to iterate through all documents while updating the command line or a Websocket
with your progress.
Built-in APIs accepting iterables
A wide variety of APIs supports iterables. Examples include the Map, WeakMap, Set, and WeakSet objects. Check out this MDN document on JavaScript APIs that accepts iterables
Conclusion
In this article, you have learned about the JavaScript iterator and how to construct custom iteration logic using iteration protocols. Also, we learned that Generator
is a function/object type that yields the kind of object value as an iterator. Although they are rarely utilized, they are a powerful, flexible feature of JavaScript, capable of dealing with infinite data streams, which can be used to build infinite scrolls on the frontend of a web application, to work on sound wave data, and more. They can maintain state, offering an effective technique to create iterators. Generators may imitate the async/await
capabilities when combined with Promises, which enables us to deal with asynchronous code in a more direct and readable way.
A TIP FROM THE EDITOR: For more on internal details of JavaScript, check out our JavaScript Types and Values, explained and Explaining JavaScript’s Execution Context and Stack articles.