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:
- 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
}
- 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
}
- 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 callsnext()
, 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 infor..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 likepush
orpop
(e.g., thearguments
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()
.