A Beginner's Guide to JavaScript Iteration

A Beginner's Guide to JavaScript Iteration

·

9 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.

  1. Initializing the iterator by calling the iterable's [Symbol.iterator]() method.

  2. Iterating over the values by successively calling the iterator's next() method.

  3. 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 the Symbol.iterator() method through bracket notation.

  • We get the length of sorcerers and initialize current to 0. current is how we keep track of our position during iteration.

  • To conform to the iterable protocol, Symbol.iterator() returns an object with a next() method.

  • Inside next(), we check if current is less than length, if it is, then done is false which means there are more items to iterate over. We get the value of the sorcerer using current as the key.

  • We increment the current value. How this works is that on the first loop, current is 0. current++ returns the old value first (0) and then increments it. On the next iteration, current is 1.

  • if current > length then that means there is nothing more to iterate over and so we return the object with donebeing true

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's next() method to produce the sequence of values to be assigned to variable.

A for...of loop exits when the iterator has completed (the iterator's next() method returns an object containing done: true).

Let’s break this down in relation to our code.

  • The for...of loop calls the [Symbol.iterator]() method on the iterable (our sorcerers object, now an iterable adhering to the iterable protocol).

  • [Symbol.iterator]() returns an iterator, an object with a next method.

  • The for...of loop calls next() until done is true, extracting the value 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:

  1. Iterable Methods: Methods like map, filter, and reduce that work on iterable objects call the iterator internally to process each element.

  2. Spread Operator: When you use the spread operator (...) with an iterable, it calls the iterator to spread the elements.

  3. 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 connect with me on Twitter. Feel free to drop a comment below—I'd love to hear your thoughts!

Happy coding!