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.
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.