DEV Community

loading...

Make an object traversable with the iterator protocol

aminnairi profile image Amin ・7 min read

Introduction

This post is a more detailed version of this post I wrote on Dev.to.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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:

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.

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.

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.

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.

Discussion (2)

pic
Editor guide
Collapse
kenbellows profile image
Ken Bellows

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!

Collapse
aminnairi profile image
Amin Author

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.