DEV Community

loading...

The Importance of Iteration Protocols

nipher profile image Jonas Mendes ・5 min read

Hey everyone, recently I caught myself talking to a great friend of mine (Maksim Ivanov, he writes some really useful articles) about iteration protocols in javascript, during the discussion we were very happy with the language improvements that these protocols bring and we also noticed even more things about it, because of this discussion, I decided to write this article.

Let's get started then, first, let me list the topics that I want to talk about:

Iteration Protocols?

Ok, if you don't even know what I'm talking about, I'll explain it real quick and show an example.

Among the iteration protocols we have the Iterable and the Iterator.

Let's check them out separately:

Iterable

This protocol simply defines a way to specify the iteration behavior of an object.

So for example, we have a string object 'Hello World', the object itself will tell how its iteration should be like.

How's that?

Well, let's see a code snippet and explain its code, I believe it'll be easier to understand.

const message = 'Hello World'

console.log(message[Symbol.iterator])
// > ƒ [Symbol.iterator]() { [native code] }

console.log(message[Symbol.iterator]())
// > StringIterator {}

So, wtf?

Let's go through it:

  • A string is created and assigned to the message constant
  • We use message[Symbol.iterator] to access a function (which will return an iterator)
  • We call the function, which returns a StringIterator (which implements the Iterator Protocol)

That's the iterable protocol, having the [Symbol.iterator] defined, so anyone can call it and get its iterator, which can be used to get the values to be iterated.

As you might have noticed, we didn't need to implement/define it, the string object already has a [Symbol.iterator] defined, it comes from its prototype (String.prototype[Symbol.iterator]), that's not the case for all types of objects, we'll talk more about it on the topic "Built-in iterables".

Iterator

This protocol is basically an interface for getting sequencial values.

If you think about it for a bit, that's what an iteration is about, getting values sequencially from something. And you'll also realize how many different types of objects we usually want to iterate over:

Arrays, Strings, Map, Set, DOM data structures, Streams, Objects...

Now, in case we want to get sequencial values from one of these guys, we could use the iterator protocol to do so.

An iterator needs to implement this interface:

{
  next() {
    return { value: <Anything>, done: <Boolean> }
  }
}

Let's use a string again, so we can see these two protocols working together:

const message = 'Hello'

const messageIterator = message[Symbol.iterator]() // > StringIterator {}

console.log(messageIterator.next())
// > { value: 'H', done: false }
console.log(messageIterator.next())
// > { value: 'e', done: false }
console.log(messageIterator.next())
// > { value: 'l', done: false }
console.log(messageIterator.next())
// > { value: 'l', done: false }
console.log(messageIterator.next())
// > { value: 'o', done: false }
console.log(messageIterator.next())
// > { value: undefined, done: true }

Well, I guess it's starting to make some sense, right?

Let's go through it real quick

  • We define the string and get an instance of its iterator from it
  • We start calling next() from the iterator instance (the interface that I mentioned)
  • Each value returned by next() is a letter from the string
  • It returns letters in a left -> right order from the string
  • When there are no letters left, we get undefined as a value and true as done (which means there're no more values)

If you think about it, it's a very simple interface and yet it brings a lot of value to the javascript language.

Its true value

As I glimpsed before, we have a lot of cases which we would like to iterate over some kind of structure/object.

We didn't really have a well defined interface for doing it, resulting in different ways to iterate over stuff.

A lot of libraries solved this problem for us, lodash for example:

_.forEach('Hello', value => console.log(key))
// > 'H' 
// > 'e'
// > 'l'
// > 'l'
// > 'o'

_.forEach([1, 2], value => console.log(value))
// > 1 
// > 2

_.forEach({ 'a': 1, 'b': 2 }, (value, key) => console.log(key))
// > 'a' 
// > 'b'

As you can see above, one single function .forEach(...) which works with any kind of object (String, Array, Object).

But it was about time, that the language itself would improve that, so we wouldn't need a library to execute such a simple thing in a programming language.

Don't get me wrong, I love lodash and such, and they're still super useful and relevant today, they themselves can use and benefit from the iteration interfaces, and they do, imagine how simpler it is to implement their _.forEach method now than it was before.

That's the real value, the combination of simplicity, consistency and well defined patterns.

for..of

So, how can we use these protocols in a generic way?

Now, we have for (let value of <iterable>) { ... }.

As you can see, it's different than the for we're used to.

Let's check some for..of examples:

const message = 'Hello'

for (let letter of message) {
  console.log(letter)
}

// > H
// > e
// > l
// > l
// > o

const list = [1, 2, 3, 4, 5]

for (let i of list) {
  console.log(i)
}

// > 1
// > 2
// > 3
// > 4
// > 5

const person = new Map([['name', 'jonas'], ['age', 23]])

console.log(person)
// > Map { name → "Jonas", age → 23 }

for (let [key, value] of person) {
  console.log(`${key}:`, value)
}

// > name: Jonas
// > age: 23

How about for..in?

It's still different.

The for..in iteration does not use iteration protocols, it iterates over enumerable properties of objects, unless the property's name is a Symbol or defined via Object.defineProperty setting enumerable to false.

This also means it would also iterate over its prototype properties (if they fit the description above).

You can avoid such thing by adding a conditional if (obj.hasOwnProperty(prop)) { ... } inside your for..in block, so it will execute the code only for properties of the actual instance.

However, you can avoid for..in if you wish, and use iteration protocols with Object instances like you would use for..in (without the necessity of the conditional though), make sure to use one of the static methods when using for..of with Object types, for example: Object.entries

I'll show how it looks like on the topic "Iterating over objects".

Creating a custom iterator

One interesting thing to point out, is that these protocols are not stricted to be implemented only in the javascript engines, it's also possible to create a custom one.

Let's check an example?

function rangeOf(n) {
  let i = 1
  const range = {}

  range[Symbol.iterator] = () => ({
    next() {
      let [value, done] = (i <= n ? [i++, false] : [undefined, true])
      return { value, done }
    }
  })

  return range
}

for (let i of rangeOf(5)) {
  console.log(i)
}

// > 1
// > 2
// > 3
// > 4
// > 5

Ok, once more, let's go through the code...

Hey, I hope you're enjoying it, read the rest of this article on my website, so I get some access and comments over there too.

I worked hard redesigning it and making it super nice for you :)

Access it Here: Post Link

Thank you!

Discussion (0)

pic
Editor guide