DEV Community

loading...

Making JS Objects iterable

IJ
Updated on ・3 min read

Disclaimer: This is a fun task that I tried doing. I don't see a real world use case for this, especially because now that we have Maps in JS. Let me know in the comments if you can think of something.

Now thats out of the way, let's get to it.

As we know, Objects in JS are not iterable. That means you cannot use them with for...of. You must've come across errors similar to:

TypeError: 'x' is not iterable

What are we trying to achieve?

We're trying to understand the technicalities behind the above error. And we will do it by making an object iterable.

What does it mean when we say iterable?

When a value is iterable, under the hood, that value has an implementation of the iterable protocol.

That means, the prototype of that element must have a method which goes like:

[Symbol.iterator](){}

..and this method is supposed to return an object like:

{
      next(){
        //we'll get to the definition of this method
      }
}
Enter fullscreen mode Exit fullscreen mode

..and this next() method will be called by the iterating functions like for...of. Each time they call next(), they expect an object of the syntax:

{ value: <value of current iteration>, done: <boolean> }

The value will be made available to the value in for(const value of element), and done will be used to know if the iteration need to be stopped or continue.

What will we do?

We'll take the object const range = {from:1, to: 5} and try to make a for...of print the values between. That is, the output should be: 1, 2, 3, 4, 5.

Let's write the code and explain what is being done.

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

  [Symbol.iterator](){
    return {
      next: () => {
        if(this.from <= this.to){
          return { value: this.from++, done: false };
        }else{
          return { done: true };
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Here we've added a new property (method) to our object, with the key Symbol.iterator. The for..of function will look for the implementation of this key, and it doesnt have it, it will throw the error we mentioned at the beginning of the blog. And as per the spec, Symbol based keys need to be created with square brackets around.

This new method returns an object (like we mentioned a bit above), which has next method in it. The logic of the next method is self explanatory. It increments the value of from till it reaches to, and on each iteration it returns an object with value and done keys in it.

When the done = false (in the last iteration), the for...of method will stop iterating it further.

Problem with the above code

If you notice, the next method is modifying the value of original property from. At the end of the iteration, it would have reached 6, which is not good. Because we dont want range = {from: 1, to: 5} to become range = {from: 6, to: 5}. So what do we do?

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

  [Symbol.iterator](){
    return {
      start: this.from,
      end: this.to,

      next(){
        if(this.start <= this.end){
          return { value: this.start++, done: false };
        }else{
          return { done: true };
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

We've added start and end variables under the local scope of the object we're returning. We could have kept the same name as from, to, but that would have created confusion while reading.

Also we've replaced the arrow function with a regular function so that the this inside the next() points to the object that we return. Otherwise next() wont have access to start and end properties.

Lets use Generators to further optimise this code

(Generator functions)[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*] were designed to solve these kind of use cases. When called, they return an object with next method in it. And that method returns something like this:

{ value: <value of current iteration>, done: <boolean> }

..which is exactly what our for..of needs.

Lets try modifying our code to use generator function.

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

  *[Symbol.iterator](){
    for(let value=this.from; value<=this.to; value++){
      yield value;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Every time iterator method gets called, the loop runs and the yield returns the value of the index(1) and pauses the execution, waiting for next call. Next time for..of calls, it resumes execution from where it paused and returns next index value(2). So and so forth till it exits the loop.

Voila! That was simple and clean. Hope you understood how iterator protocol, and generators work.

Discussion (0)