Skip to main content

Building Blocks in JS

Iterables

JavaScript is special—it’s versatile, capable, and packed with tools that make life easier. One such tool is the concept of Iterables. You’ve probably used them without even realizing it. Remember that trusty for..of loop? Or how you’ve looped through an array or string? Behind the scenes, iterables are the unsung heroes making it all possible.

In this article, we’ll dive deep into the world of iterables and iterators in JavaScript, understand their magic, and see how to use it effectively. Along the way, we’ll explore built-in iterables, custom iterable objects, and how to convert between iterables and other data structures.

So, ready?

What Exactly Are Iterables?

Basics: What are iterables? In simple terms, an iterable is an object that can be looped over. Arrays, strings, maps, and sets are examples of built-in iterables in JavaScript. The key to making an object iterable is implementing a special method called [Symbol.iterator].

When an object implements this method, it becomes iterable, meaning you can use a for..of loop to iterate through its values.

Built-in Iterables

You’ve likely encountered some built-in iterables already. These include:

  1. Arrays: The classic data structure for holding ordered collections of items.
const arr = [1, 2, 3, 4];
for (let item of arr) {
  console.log(item); // Output: 1, 2, 3, 4
}
  1. Strings: A string in JavaScript is essentially an iterable collection of characters.
for (let char of "Hello") {
  console.log(char); // Output: H, e, l, l, o
}
  1. Maps and Sets: These are relatively newer additions to JavaScript, but they follow the same iterable principles.
const map = new Map([['a', 1], ['b', 2]]);
for (let [key, value] of map) {
  console.log(`${key}: ${value}`); // Output: a: 1, b: 2
}

const set = new Set([1, 2, 3]);
for (let value of set) {
  console.log(value); // Output: 1, 2, 3
}

Symbol.iterator: How iterables work

Under the hood, iterables work through the Symbol.iterator method. This method is called by the for..of loop and returns an iterator object. The iterator object has a method called next(), which returns an object containing:

  • value: The next value in the sequence.
  • done: A boolean indicating whether the iteration is complete.

To make things clearer, let’s create our own iterable object!

Custom Iterables:

Imagine you have an object that represents a range of numbers, and you want to loop through it using for..of. This is the perfect scenario for custom iterables. Let’s create a simple range object that is iterable.

let range = {
  from: 1,
  to: 5
};

// Add Symbol.iterator to make range iterable
range[Symbol.iterator] = function() {
  return {
    current: this.from,
    last: this.to,
    
    next() {
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// Now you can use for..of with range
for (let num of range) {
  console.log(num); // Output: 1, 2, 3, 4, 5
}

Let’s break down what’s happening here:

  • The range object isn’t an array, but we’ve made it iterable by implementing the [Symbol.iterator] method.
  • This method returns an iterator object, which has a next() method.
  • Each time the for..of loop runs, it calls next(), getting the next value in the sequence until the iteration is done.

Iterator vs Iterable

It’s important to understand the distinction between iterables and iterators:

  • An iterable is an object that can be iterated over (has a [Symbol.iterator] method).
  • An iterator is an object that is returned by the iterable’s [Symbol.iterator] method. It knows how to access each value in the iterable.

The neat part is that you can even combine them! Here’s a slightly shorter version of the range example:

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },

  next() {
    if (this.current <= this.to) {
      return { done: false, value: this.current++ };
    } else {
      return { done: true };
    }
  }
};

for (let num of range) {
  console.log(num); // Output: 1, 2, 3, 4, 5
}

Now, range itself serves as both the iterable and the iterator, but keep in mind that this approach has limitations. If you run two for..of loops on the same object simultaneously, they’ll share the same state. But for most use cases, this method works just fine.

Infinite Iterables

Just because an iterable object can have a finite number of values doesn’t mean it has to. You can create infinite iterables as well! Let’s make an infinite iterable that generates an infinite sequence of random numbers.

let randomNumbers = {
  [Symbol.iterator]() {
    return {
      next() {
        return { done: false, value: Math.random() };
      }
    };
  }
};

for (let num of randomNumbers) {
  console.log(num); // Infinite loop of random numbers!
  if (num > 0.9) break; // Exit condition to stop the loop
}

This generates random numbers endlessly until you explicitly stop the loop using break.

Iterables vs array-like objects

When working with JavaScript, you may come across two terms: iterables and array-like objects. They sound similar but are quite different.

  • Iterables are objects that have the [Symbol.iterator] method, allowing them to be used in for..of loops (e.g., arrays, strings).
  • Array-like objects are objects that have indexed elements and a length property but don’t have array methods like push or pop (e.g., the arguments object in functions).

Here’s an example of an array-like object:

let arrayLike = {
  0: "Hello",
  1: "World",
  length: 2
};

// Error! arrayLike is not iterable
for (let item of arrayLike) {
  console.log(item);
}

Array-like objects can’t be used with for..of because they don’t have a [Symbol.iterator] method. But don’t worry, we can easily convert them into a proper array!

Converting Iterables and array-like objects

Array.from():

Array.from(), a utility that converts both iterables and array-like objects into actual arrays. This is super useful when you want to work with array methods on these objects.

Let’s convert our previous arrayLike object:

let arrayLike = {
  0: "Hello",
  1: "World",
  length: 2
};

let arr = Array.from(arrayLike);
console.log(arr); // Output: ["Hello", "World"]

We can even use Array.from() on our custom iterable:

let arrFromRange = Array.from(range);
console.log(arrFromRange); // Output: [1, 2, 3, 4, 5]

Mapping with Array.from()

Array.from() also takes an optional mapping function, allowing you to transform each element while converting.

let squaredRange = Array.from(range, num => num * num);
console.log(squaredRange); // Output: [1, 4, 9, 16, 25]

Now we’re cooking with gas! You’ve turned your iterable into an array and even transformed the values along the way.

Iterating over strings

Strings are built-in iterables, which means you can iterate over them character by character.

for (let char of "JavaScript") {
  console.log(char); // Output: J, a, v, a, S, c, r, i, p, t
}

Handling Surrogate Pairs

JavaScript strings are Unicode-friendly, but some characters (like emojis) are made up of two 16-bit code units. These are called surrogate pairs. The good news? JavaScript’s for..of loop handles these beautifully.

let str = '𝒳😂';
for (let char of str) {
  console.log(char); // Output: 𝒳, 😂
}

Each emoji (or any character formed by a surrogate pair) is treated as a single character during iteration.

Conclusion

Iterables are a fundamental part of JavaScript’s flexibility when it comes to looping and working with data collections. Whether you’re using arrays, strings, or creating your own custom iterables, understanding how iterables work gives you the power to write cleaner, more efficient, and more expressive code.

By the end of this article, you should be comfortable with:

  • Understanding and using built-in iterables like arrays and strings.
  • Creating custom iterable objects.
  • Converting between iterables and array-like objects with Array.from().