Make an object traversable with the iterator protocol
(Source/Credits: https://dev.to/aminnairi/make-an-object-traversable-with-the-iterator-protocol-3ilo)
Meta-programming in JavaScript with the iterator protocol
title: Make an object traversable with the iterator protocol published: true description: Meta-programming in JavaScript with the iterator protocol tags: javascript,webdev,tutorial,iterators
Introduction
This post is a more detailed version of this post I wrote on Dev.to.
{% link aminnairi/let-s-build-a-garage-2mbn %}
It will be based on a similar example so if you followed what has been said before you should not be lost while reading this article.
Let's say I have an object that describe some specifications about a motorcycle.
javascript
const motorcycle = {
brand: "Triumph",
model: "Street Triple",
year: 2018
}
I want to iterate through all the specifications of that motorcycle. One way we could do that is to use the getOwnPropertyNames
method from the Object
object. It returns an array that we can iterate over.
```javascript for (const key of Object.getOwnPropertyNames(motorcycle)) { console.log(key) }
// brand // model // year ```
Now that we have the key names from our object, we can get the value for that property quite easily using the bracket notation.
``javascript
for (const key of Object.getOwnPropertyNames(motorcycle)) {
console.log(
${key}: ${motorcycle[key]}`)
}
// brand: Triumph // model: Street Triple // year: 2018 ```
What I am about to show you is a way to turn an object into an iterable object. This will be quite a mouthful so we will use a function to wrap this behavior in order to have something re-usable and turn N objects into iterable objects easily.
The iterator protocol
We said that we wanted a function to turn any object into an iterable object. Let's create that function.
javascript
function toIterable(target) {
// ...
}
What this function will do is add a special property that will be detected by the JavaScript runtime as an iterator. This special property is called Symbol.iterator
. Its value will be a function that will be run whenever we want to iterate this object. Typically, the for...of
loop will check that the object is indeed an iterator and will run that special function for us in the background. Others function and idioms will do that such as the from
method of the Array
object.
javascript
function toIterable(target) {
Object.defineProperty(target, Symbol.iterator, {
value: function() {
// ...
}
})
}
Now, what we have to do is implement the iterator protocol. See that as an interface, where you have to provide a way to represent all the iterations out of your object.
Implementing the iterator protocol in JavaScript means returning an object formatted in an unique way. This object will contain a method called next
that is used internally by the all the functions and idioms that accept an iterable object and will call this function to get the iterations, one by one. One way to represent this schema is with the following code.
javascript
myObject[Symbol.iterator].next() // First iteration
myObject[Symbol.iterator].next() // Second iteration
myObject[Symbol.iterator].next() // undefined, meaning this is the last iteration
That is what happens behind the scenes when you try to iterate over an array. The for
loop is just a syntactic sugar around this behavior. But ain't nobody got time for that...
Let's try to implement this behavior in our function.
```javascript function toIterable(target) { Object.defineProperty(target, Symbol.iterator, { value: function() { // ...
const iterator = {
next() {
// ...
}
}
return iterator
}
}) } ```
Now that we have our structure, we have to tell the function next
how to behave when something is requesting an iteration out of our object. This is where things get specific to one or another object. What I will do here is a very simple example of what we could return, but of course you may want to add some special behavior for special objects of course.
```javascript function toIterable(target) { Object.defineProperty(target, Symbol.iterator, { value: function() { // ...
const iterator = {
next() {
// ...
return { done: true, value: undefined }
}
}
return iterator
}
}) } ```
The iterator protocol specifies the format of the value that the next
method should return. It is an object, that contains two properties:
- A done
property that will tell the executor whether we are finished (or not). This means that we return done: true
when we are finishing the iteration, and done: false
when we are not. Pretty straight forward.
- A value
property. Of course, the looping would be pointless if the object has no value to return. This is where you will have the opportunity to format the value gathered by the loop. Be creative and make something special here or be simple and just return a simple value. This is what I will do.
It is worth noticing that when returning the last iteration, we can simply set the value
property to undefined
as this is only used internally by the loop to know whether we are finishing the iteration and will not be used other than for that purpose.
Now, we can add a little custom logic for gathering properties from an object and returning an iteration for each one of these.
```javascript function toIterable(target) { Object.defineProperty(target, Symbol.iterator, { value: function() { const properties = Object.getOwnPropertyNames(target) const length = properties.length
let current = 0
const iterator = {
next() {
if (current < length) {
const property = properties[current]
const value = target[property]
const iteration = {
done: false,
value: `${property}: ${value}`
}
current++
return iteration
}
return { done: true, value: undefined }
}
}
return iterator
}
}) } ```
Here, I define an index variable called current
to know where I an in the iteration process. I also gathered all properties named and stored them inside the properties
variable. To know when to stop, I need to know how many properties I have with the length
variable. Now all I do is returning an iteration with the property name and value and incrementing the current index.
Again, this is my way of iterating over an object and you could have a completely different way of formating your values. Maybe you could have a files
object and using fs.readFile
to read the content of the file before returning it in the iteration. Think out of the box and be creative! I actually think that this will be a good exercise for the reader to implement a fileReaderIterator
function that will do exactly that if you are using Node.js.
Of course, putting it all together will give us the same result as previously.
```javascript toIterable(motorcycle)
for (const characteristic of motorcycle) { console.log(characteristic) }
// brand: Triumph // model: Street Triple // year: 2018 ```
Even though we wrote a lot of code, this code is now reusable through all the object we want to make an iterable of. This also has the advantage to make our code more readable than before.
Generators
What we saw is a working way of creating an iterable. But this is kind of a mouthful as said previously. Once this concept is understood, we can use higher level of abstraction for this kind of purpose using a generator function.
A generator function is a special function that will always return an iteration. This is an abstraction to all we saw previously and helps us write simpler iterators, leaving more space for the inner logic rather than the iterator protocol implementation.
Let's rewrite what we wrote earlier with this new syntax.
```javascript function toIterable(target) { Object.defineProperty(target, Symbol.iterator, { value: function*() { for (const property of Object.getOwnPropertyNames(target)) { const value = target[property]
yield `${property}: ${value}`
}
}
}) } ```
Notice the star after the function
keyword. This is how the JavaScript runtime identifies regular function from generator functions. Also, I used the yield
keyword. This special keyword is an abstraction to the iteration we had to manually write before. What it does is returning an iteration object for us. Cool isn't it?
Of course, this will also behave exactly like what we had earlier.
```javascript for (const characteristic of motorcycle) { console.log(characteristic) }
// brand: Triumph // model: Street Triple // year: 2018 ```
Iterable classes
Have you ever wanted to iterate over an object? Let's say we have a class Garage
that handle a list of vehicles.
```javascript class Garage { constructor() { this.vehicles = [] }
add(vehicle) { this.vehicles.push(vehicle) } }
const myGarage = new Garage()
myGarage.add("Triumph Street Triple") myGarage.add("Mazda 2") myGarage.add("Nissan X-Trail") ```
It could be useful to iterate through our garage like so:
``javascript
for (const vehicle of myGarage) {
console.log(
There is currently a ${vehicle} in the garage`)
}
// TypeError: myGarage is not iterable ```
Aouch... That's a shame. How cool it would be if that would work... But wait a minute, we can make it work! Thanks to the iterator protocol and generators.
```javascript class Garage { constructor() { this.vehicles = [] }
add(vehicle) { this.vehicles.push(vehicle) }
*Symbol.iterator { for (const vehicle of this.vehicles) { yield vehicle } } } ```
What I used here is just a shorthand syntax to what we did above, and has the exact same effect: it defines a property called Symbol.iterator
that is a generator function returning an iteration out of our object. In a nutshell we just made our object iterable.
``javascript
for (const vehicle of myGarage) {
console.log(
There is currently a ${vehicle} in the garage`)
}
// There is currently a Triumph Street Triple in the garage // There is currently a Mazda 2 in the garage // There is currently a Nissan X-Trail in the garage ```
But this does not stop here. We are also able to use every methods that take an iterable as their parameters. For instance, we could filter out all vehicles taking only the Triumphs motorcycles.
```javascript Array.from(myGarage).filter(function(vehicle) { return vehicle.includes("Triumph") }).forEach(function(triumph) { console.log(triumph) })
// Triumph Street Triple ```
And there we go. Our instance has now became something iterable. We now can use all the powerful methods linked to the Array
object to manipulate our object easily.
Comments section
kenbellows
•May 1, 2024
Awesome overview! IMO iterators and generators are super underrated features of JavaScript. The Iterator protocol is definitely a little confusing, but generators make it way easier. It gets even more amazing when you add in async iterators, generators, and loops for iterating through sequential asynchronous processes! No more awkward Promise reducers necessary!
aminnairi Author
•May 1, 2024
Really appreciate your comment Ken, thank you. I Couldn't have said better as a conclusion. I think I'll do an overview of async generators in order to complete the circle.