A Beginner's Guide to JavaScript Iteration
#iterators, #iterables, #iteration protocols
Dec 20, 202310 min read
Gun to your head.
10 seconds.
Tell me what iterators are.
If you fumbled the answer or just straight up didn’t know… well, good luck Charlie. Just kidding, it just means I’ve reached my target audience.
Also pretty wild start to the article right? I promise there are no more threats going forward. Just good vibes and a breakdown of all things iterators.
Introduction
An often-overlooked powerhouse is iterators. They’re rarely in the spotlight, but they play a crucial role in how we handle data. In fact, they're at the core of what makes JavaScript tick.
Iterators work quietly behind the scenes, helping us navigate through data more efficiently. In this blog, we'll take a closer look at iterators, how they make iteration possible and how you can build your own.
Iterators and the Iterator Protocol
If iterators had you scratching your head at the beginning, you're not alone. Luckily, understanding them is well within reach. It may seem daunting, but stick with me, and we'll unravel it together.
Let's start by understanding iteration since you'll encounter the term frequently.
It's quite common in coding to repeat tasks until a certain condition is met. Manually copying code is not only tedious but goes against the principle of keeping things DRY (Don't Repeat Yourself). This is where iteration becomes valuable. It's the automated repetition of instructions until a specific condition is satisfied.
If you've started thinking of loops, you're heading in the right direction. Though, at the foundation of those loops, it's iterators that make it all possible.
What are Iterators?
Iterators provide a structured way to access elements sequentially. In simpler terms, they allow you to navigate through a collection of data, fetching one item at a time. Think of them as your guide through a list, keeping track of your location and pointing you to the next item.
At its core, an iterator is just an object. That’s it. What makes it special, is that it’s an object that adheres to a set of rules known as the iterator protocol. But what exactly is this protocol?
The Iterator Protocol
The iterator protocol outlines the procedure for producing a sequence of values, one at a time, from an object. This protocol says that an object qualifies as an iterator when it implements the next()
method. Remember, a method is simply a property in an object that holds a function.
The next()
method is a function that accepts at most, one argument and returns an object satisfying the IteratorResult
interface. This means the object must include the following properties:
value
- the current value in the iteration.done
- a boolean that indicates whether the iteration is finished (true
when done,false
when more items are available).
Both properties are optional; if an object without either property is returned, it's practically equivalent to { done: false, value: undefined }
. It's akin to signalling, "There are more items left—keep going!”
Now that we've covered iterators, let's shift our focus to iterables. These counterparts collaborate with iterators, working together to produce values in sequence. Let’s explore the relationship between the two.
TL;DR
Iterators help you go through data step by step using the next()
method. The iterator protocol says that next()
should return an object with a value
and done
property.
Iterables and the Iterable Protocol
Iterators create iterables, and they are used to loop over those iterables. But if that’s the case, what makes something an iterable?
Have you ever wondered why you can use a for...of
loop with an array but not with an object literal? Well, it turns out objects are not iterables.
Let’s look at a simple example. (This is me hinting that you should go watch Arrested Development by the way. 😉)
Back to the example.
const characters = ['Maybe', 'George Micheal', 'Gob', 'Lucille'];
const series = {
name: 'Arrested Development',
};
If we inspect the prototype of the characters
array we’ll find a property called [Symbol.iterator]()
but if we inspect the prototype of the series
object we’ll find that the [Symbol.iterator]()
property is missing.
Why? It’s simple. Objects are not iterables.
If we try to loop over series
with a for...of
loop, JavaScript is gonna slap us with TypeError: series is not iterable
to stress that series
is not an iterable object.
Iterables are objects that adhere to the iterable protocol. This protocol lets objects define their iteration behaviour. When an object implements the iterable protocol it gains the ability to be traversed or iterated using a for...of
loop or other iteration methods.
In order to be an iterable, the object must implement the @@iterator
method. This @@iterator
key should be available via the [Symbol.iterator]()
property. Simply put, iterables are objects with a [Symbol.iterator]()
property.
[Symbol.iterator]()
is a method that accepts no arguments and returns an iterator that conforms to the iterator protocol. Therefore, the [Symbol.iterator]()
method should return an object with a next()
method.
Here’s an example.
// Implementation of Symbol.iterator method
const someIterable = {};
someIterable[Symbol.iterator] = function () {
return {
next() {
// some logic
},
};
};
TL;DR
Iterables are objects that have a [Symbol.iterator]()
property that returns an iterator — an object with a next()
method.
Manual Iteration with the Iterator Object
Before we dive into how iteration works with for...of
, let’s have a closer look at the role [Symbol.iterator]()
plays.
Let’s revisit our characters
array.
const characters = ['Maybe', 'George Micheal', 'Gob', 'Lucille'];
How could we get the values sequentially without using any iteration constructs? Well, this is where [Symbol.iterator]()
shines.
const iterator = characters[Symbol.iterator]()
console.log(iterator)
// the iterator object returned from characters[Symbol.iterator]()
Object [Array Iterator] {
__proto__: { next: ƒ next() }
}
Calling characters[Symbol.iterator]()
yields an iterator object. We can leverage this object to step through the characters
array sequentially.
console.log(iterator.next()); // { value: 'Maybe', done: false }
console.log(iterator.next()); // { value: 'George Micheal', done: false }
console.log(iterator.next()); // { value: 'Gob', done: false }
console.log(iterator.next()); // { value: 'Lucille', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
console.log(iterator.next()); // { value: undefined, done: true }
On the iterator's prototype resides the next()
method. With each call, it returns an IteratorResult
containing the current value
and the done
status. When done
is true
, subsequent calls return { value: undefined, done: true }
.
Beneath the surface, iteration methods are just following these steps.
- Initializing the iterator by calling the iterable's
[Symbol.iterator]()
method. - Iterating over the values by successively calling the iterator's
next()
method. - Looping until the iterator signals completion (
{ done : true }
), terminating the iteration.
Knowing these steps, you’re ready to create your own iterables!
User-defined Iterables
Remember how I said we couldn’t use a for...of
loop with objects, well then what can we use them with?
While object literals don't naturally integrate with the for...of
loop, there are other objects we can use them with. The for...of
loop is specifically tailored for working with iterables, and in JavaScript, several built-in data structures inherently support iteration. The most common ones are Arrays
, Strings
, Maps
and Sets
.
You can read more about them here.
Earlier, we highlighted that iterators make iteration possible, but let's delve into how exactly that happens and where the for...of
loop fits into the equation. Let's consolidate our knowledge and see how these components collaborate.
We can get a better understanding by creating our own iterable from an object!
(Hint! Hint! Coughs in jujutsu kaisen)
Consider this object.
const sorcerers = {
0: 'Itadori',
1: 'Nanami',
2: 'Gojo',
3: 'Megumi',
4: 'Kugisaki',
};
We want to loop over the sorcerers object with a for...of
loop but since it’s not an iterable, we will transform it into one by adding the [Symbol.iterator]()
key to it. Here’s how we’d achieve that.
sorcerers[Symbol.iterator] = function () {
const length = Object.keys(this).length;
let current = 0;
return {
next() {
if (current < length) {
return { value: sorcerers[current++], done: false };
} else {
return { value: undefined, done: true };
}
},
};
};
Cool, right? But how’s this working? Let’s take a look at the [Symbol.iterator()
method first.
- We transform
sorcerers
to an iterable by dynamically adding theSymbol.iterator()
method through bracket notation. - We get the length of
sorcerers
and initializecurrent
to0
.current
is how we keep track of our position during iteration. - To conform to the iterable protocol,
Symbol.iterator()
returns an object with anext()
method. - Inside
next()
, we check ifcurrent
is less thanlength
, if it is, thendone
isfalse
which means there are more items to iterate over. We get the value of the sorcerer usingcurrent
as the key. - We increment the
current
value. How this works is that on the first loop,current
is0
.current++
returns the old value first (0
) and then increments it. On the next iteration,current
is1
. - if
current > length
then that means there is nothing more to iterate over and so we return the object withdone
beingtrue
Now we can iterate the object with a for of loop.
for (let sorcerer of sorcerers) {
console.log(sorcerer);
}
// 'Itadori'
// 'Nanami'
// 'Gojo'
// 'Megumi'
// 'Kugisaki'
TL;DR
While object literals don't naturally work with the for...of
loop, we can manually transform them into iterables by adding a [Symbol.iterator]()
method. This method enables iteration by returning an object with a next()
method, allowing compatibility with the for...of
loop.
For...of and other Looping Constructs
Let's take a moment to understand for...of
purpose and how it simplifies the process of traversing iterables. Here’s an excerpt from MDN.
for...of
When a
for...of
loop iterates over an iterable, it first calls the iterable's[@@iterator]()
method, which returns an iterator, and then repeatedly calls the resulting iterator'snext()
method to produce the sequence of values to be assigned tovariable
.A
for...of
loop exits when the iterator has completed (the iterator'snext()
method returns an object containingdone: true
).
Let’s break this down in relation to our code.
- The
for...of
loop calls the[Symbol.iterator]()
method on the iterable (oursorcerers
object, now an iterable adhering to the iterable protocol). [Symbol.iterator]()
returns an iterator, an object with anext
method.- The
for...of
loop callsnext()
untildone
istrue
, extracting thevalue
property and returning it.
While the for...of
loop is a prominent use case for iterators, several other JavaScript constructs also leverage them behind the scenes:
- Iterable Methods: Methods like
map
,filter
, andreduce
that work on iterable objects call the iterator internally to process each element. - Spread Operator: When you use the spread operator (
...
) with an iterable, it calls the iterator to spread the elements. - Array Destructuring: When you use array destructuring, it calls the iterator to assign values to variables.
TL;DR
The for...of
loop along with other built-in iterators, internally calls the [Symbol.iterator]()
method and iterates until completion.
Conclusion
Iteration methods aren't elusive; fundamentally, they rely on iterators.
Iterators allow sequential access to an object's elements while adhering to the iterator protocol. Their counterparts, iterables, are simply objects with a [Symbol.iterator]()
property that returns an iterator—an object with a next()
method.
With these iteration protocols, we can create custom iterables and customize their iteration behaviour.
Thanks for reading! If you found this article helpful or have questions, you can connect with me on Twitter.
Happy coding!
Back to Blogs