author: Krasimir Tsonev

Krasimir is a blogger, who writes and speaks.
He loves open source and codes awesome stuff.

Make your objects iterable

Just recently I became a fan of the iterable protocol. I knew about it for some time now but never actually integrated it on my own. I ended up using it to provide a nice API for one of my libraries and I thought that this approach worth sharing.

What is iterable protocol

The iterable protocol allows JavaScript objects to define or customize their iteration behavior.

source

Or in other words there are statements in JavaScript that work only with objects that implement that protocol. For example for...of:

const arr = ['a', 'b', 'c'];

for (const item of arr) {
  console.log(item);
}

// output
// "a"
// "b"
// "c"

That works because the JavaScript built-in Array implements the iterable protocol. And to call a given object "iterable" that object needs to have a @@iterator method. Consider the code below:

const arr = [1, 2, 3, 4];

console.log(arr[Symbol.iterator])
// outputs:
// ƒ values() { [native code] }

Symbol.iterator is a constant that is basically @@iterator.

Every time when we need to iterate over an object its @@iterator method is called. That method should return an iterator.

Iterator

Iterator is every object that has a .next method. Once called that method should return another object with done and value keys. Imagine that we have a list of items. done will be true when we go over all the items. Each time next is fired we return the next item from the list. Here's is an example:

const user = {
  firstName: 'Krasimir',
  lastName: 'Tsonev',
  skills: ['JavaScript', 'HTML', 'CSS'],
  [Symbol.iterator]: function () {
    let i = 0;

    return {
      next: () => ({
        value: this.skills[ i++ ],
        done: i > this.skills.length
      })
    }
  }
}

for (const skill of user) {
  console.log(skill);
}
// output:
// "JavaScript"
// "HTML"
// "CSS"

user is definitely not an array but we can iterate over it because we implemented the iterable protocol.

Slick interface for your objects

That's cool but I have another use case which became my favorite. That same protocol is used when we destruct stuff. For example:

const a = ['a', 'b', 'c'];
const [ first, , last ] = a;

console.log(first);
console.log(last);
// outputs:
// "a"
// "c"

Now let's return to the code in the previous section. The one where we define the user object. Imagine that we need to create a method for changing the name of the user and also to add a new skill. We'll probably go with the following:

const user = {
  firstName: 'Krasimir',
  lastName: 'Tsonev',
  skills: ['JavaScript', 'HTML', 'CSS'],
  changeName(newName) {
    const parts = newName.split(' ');
    this.firstName = parts[0];
    this.lastName = parts[1];
  },
  addSkill(newSkill) {
    this.skills.push(newSkill);
  }
}

user.changeName('Jon Snow');
user.addSkill('React');

console.log(`${ user.firstName } ${ user.lastName }`);
console.log(user.skills);

// outputs:
// Jon Snow
// (4) ["JavaScript", "HTML", "CSS", "React"]

That works but what about the following API:

const [ changeUserName, addUserSkill ] = user;

changeUserName('Jon Snow');
addUserSkill('React');

Of course that's not really possible if we don't implement a proper iterator. The one above was looping over the skills array. Let's define a new one called api that holds our changeName and addSkill methods.

const user = {
  firstName: 'Krasimir',
  lastName: 'Tsonev',
  skills: ['JavaScript', 'HTML', 'CSS'],
  [Symbol.iterator]: function () {
    const api = [
      newName => {
        const parts = newName.split(' ');
        this.firstName = parts[0];
        this.lastName = parts[1];
      },
      newSkill => {
        this.skills.push(newSkill);
      }
    ]
    let i = 0;

    return {
      next: () => ({
        value: api[ i++ ],
        done: i > api.length
      })
    }
  }
}

const [ changeUserName, addUserSkill ] = user;

changeUserName('Jon Snow');
addUserSkill('React');

console.log(`${ user.firstName } ${ user.lastName }`);
console.log(user.skills);

We used destructing to gain access to methods on the user object. What I like here is the fact that I have to deal with just functions and forget about the existence of the user. Of course we can achieve the same thing by using bind:

const changeUserName = user.changeName.bind(user);

but still const [ changeUserName, addUserSkill ] = user reads better to me. Especially after months using React hooks.


P.S. If you liked what you just learned and now want to write iterators everywhere - think twice. The biggest downside of this approach is that your methods land outside in specific order. So you have to always explain/remember what is what.


blog comments powered by Disqus