Javascript Iterators made easy
With ES6, we got new data types, new syntaxes, and new built-ins, but something that is not often mentioned is the new protocols that were added to the language. Two useful protocols are the iterable protocol and the iterator protocol that was added to define a standard as to how objects are iterable and how their data is consumed when iterated over. We can customize how we iterate over objects with for...of
loops. In this article, we will go over these protocols and look at some examples creating custom iterables.
The Iterable Protocol
The iterable protocol defines how an objects iteration behavior is implemented. An object is iterable via the iterator method @@iterator
. This method is required for any object to be considered iterable. We can implement this method with the Symbol.iterator
constant. We will see some examples of how to implement custom iterables below.
The Iterator Protocol
Javascript iterators are objects that define an iterable sequence and the potential return value of the iteration. Iterators implement the Iterator protocol defined by implementing a next()
method that returns an object with a done
(boolean) property and a value
property. An iterator object can be iterated by calling the next
method until it returns done: true
. Once the iterator is done, future calls will return done: true
as well. In short ES6 iterators provide a newly defined way to iterate over a collection.
Photo by Evan Dvorkin on Unsplash
Fibonacci Sequence Iterator
Since Iterators are just objects that implement the protocol. Functions can be used to create iterators. In this example, we will create an iterator that returns a fixed set of the Fibonacci sequence. We will pass in the size of the sequence that we want to be returned and then use the for...of
to produce our sequence from the iterator. It looks like this:
function fibonacci(end = 1) {
let n1 = 1
let n2 = 1
let count = 0
return {
// The Symbol.iterator function returns this -- the iterable
[Symbol.iterator]() {
return this
},
// The next function -- the iterator
next() {
// Break out of the iterator by returning done
// when the end of the sequence is reached
if (count >= end) return { done: true }
const current = n2
n2 = n1
n1 = n1 + current
count++
return { value: current, done: false }
}
}
}
// for...of is used to consume the iterator
for (const num of fibonacci(6)) {
console.log(num)
}
// => 1
// => 1
// => 2
// => 3
// => 5
// => 8
// We can use the spread operator too
console.log(...fibonacci(4))
// => 1 1 2 3
Notice that the object returned by our Fibonacci factory function defines the two key properties that an iterator must-have. It has the @@Iterator
defined by Symbol.iterator
(without this we will get an error saying: TypeError: fibonacci(...) is not a function or its return value is not iterable
) and the next()
method that returns an object with a value property and done: false
or done: true
property. The object when iterated returns our fibonacci sequence. We can also call fibonacci()
and it will return the iterator object and we can manually iterate over the sequence by calling the next()
method directly.
const sequence = fibonacci(2)
console.log(sequence.next().value)
// => 1
console.log(sequence.next().value)
// => 1
console.log(sequence.next().done)
// => true
Notice that the since the sequence was limited to two, that calling next a third time returns done equals true. The iterator has been consumed and all further calls will return the same.
Custom object iterator
If I gave you an array of strings and asked you to print each one to the console it would be pretty easy. You would probably do something like this:
const cities = [
'Miami',
'Tampa Bay',
'Los Angeles',
'San Francisco',
'Dallas',
'Houston',
]
for(let i = 0; i < cities.length; i++) {
console.log(cities[i])
}
// => Miami
// => Tampa Bay
// => Los Angeles
// => San Francisco
// => Dallas
// => Houston
Easy right? But what if I gave you a more complex data structure to deal with, like this:
const cities = {
citiesByState: {
flordia: ['Miami', 'Tampa Bay'],
california: ['Los Angeles', 'San Francisco'],
texas: ['Dallas', 'Houston']
}
}
Here we have an object of cities and broken down by state. We can imagine that this could become even more complex and in fact, I would expect a real-world case to be more complex. A city might be an object with a name property and a bunch of other data etc. To handle this we might start writing nested for loops and everything could become really messy really fast. But with iterators, we now have a defined standard to make this object iterable.
const cities = {
citiesByState: {
flordia: ['Miami', 'Tampa Bay'],
california: ['Los Angeles', 'San Francisco'],
texas: ['Dallas', 'Houston']
},
[Symbol.iterator]() {
// Get all the states in an array
this.states = Object.values(this.citiesByState)
this.currentState = 0
this.currentCity = 0
return this
},
next() {
const state = this.states[this.currentState]
// If there is no state left return done equals true
// to stop iterating
if (!state) return { done: true }
const city = state[this.currentCity]
// When we reach the end of the cities in a state increment the state
if (this.currentCity === state.length - 1) {
this.currentState++
this.currentCity = 0
} else {
// Increment the city each time until the end is reached
this.currentCity++
}
return { value: city, done: false }
}
}
// for...of lets us consume the custom iterator
for (let city of cities) {
console.log('City name: ', city)
}
// => Miami
// => Tampa Bay
// => Los Angeles
// => San Francisco
// => Dallas
// => Houston
// We can use the spread on the cities now too
console.log(...cities)
// => Miami Tampa Bay Los Angeles San Francisco Dallas Houston
We have seen two ways that we can implement the Iteration protocols defined by the ES6 spec. Many built-ins already have the iterable protocol implemented. Arrays. Maps, Sets, and Strings already have it and there are several ways you can consume these iterables. Javascript also has the for..in
loop that allows you to iterate over an objects keys without implementing a custom iterator. Arrays are the most commonly understood iterable and many built-in methods are provided to consume them. We can consume them directly if we want like this:
const array = [1, 2, 3]
// get the array @@iterator function and call it
const arrayIterator = array[Symbol.iterator]()
console.log(arrayIterator.next().value)
// => 1
console.log(arrayIterator.next().value)
// => 2
console.log(arrayIterator.next().value)
// => 3
We would not normally do this, but I want to show that these built-ins use the same protocol that we can use to create our own custom Iterators.
It is actually rare that you would need to implement a custom iterable or iterator protocol, but it is very helpful to understand them and what is happening under the hood of our Javascript. They are also critical to understand when we discuss our next topic Generators. Check it out here Javascript Generators made easy. As always, Happy Coding!
Other articles on Iterators
EasyCoders always wants to give you the best resources, so here are some other good resources on iterables.