Iteratify - Make It Iterable

Among the new features brought by the ES6, we find the addition of the primitive type Symbol and the definition of the iterator's interface.
In this post, we use both to make an object iterable. While the usefulness of this functionality is questionable (easily replaceable by the use of Object.entries or similar) it will allow us to focus attention on the subject.

Impl

What

The iteratify function takes an object as a parameter and returns an iterable copy of it.

const object = {
  foo: true,
  bar: 'hello',
  baz: 42,
}

const itObject = iteratify(object)

for (let(key, val) of itObject) {
  console.log(key, val)
}

Where

When trying to apply a for loop or spread operator on a type in JavaScript, what happens under the hood is the execution of the method under the Symbol.iterator label. The current state:

typeof object[Symbol.iterator] === 'function' // returns false

It is absolutely valid to add the method directly in the object literal:

const object = {
    ...,
    [Symbol.iterator]() {
        ...
    }
}

However, this means that the [Symbol.iterator] method would be enumerable. It's not the case. It is easily solved:

function iteratify(obj) {
  // create a copy of object (supposing it is flat for simplicity)
  const copy = Object.assign({}, obj)

  Object.defineProperty(copy, Symbol.iterator, {
      enumerable: false,
      writable: true,
      configurable: true,
      value: // next step
  })

  return copy
}

How

ES6 has standardized the interface for the Iterator. It is a method that when executed returns an object. This must necessarily contain a next method. At each execution of the latter, an IteratorResult is obtained, that is an object that necessarily contains two specific properties:

  • value - the value generated for the current iteration. Can be any type.
  • done - a boolean representing the state of the iterator.
function iteratify(obj) {
  const copy = Object.assign({}, obj)

  Object.defineProperty(copy, Symbol.iterator, {
    enumerable: false,
    writable: true,
    configurable: true,
    value: iterator,
  })

  return copy

  function iterator() {
    const entries = Object.entries(copy)
    let i = 0

    return {
      next() {
        if (i < entries.length) {
          return { value: entries[i++], done: false }
        }

        return { done: true } // implicit: value: undefined
      },
    }
  }
}

In this case calling next gets an IteratorResult whose value is the entry to the index i - also i++ happens, so the next time next is called it will return the next entry.

function iterator() comes after the return. Isn't that dead code?
No. function hoisting

Usage

Invocation of next? And when in the world?
In case you delegate the iteration to the for...of loop, the JavaScript internal calls next repeatedly until an IteratorResult is returned whose done is true. However, you can "manually" call next as follows:

const itObject = iteratify({
  foo: true,
  bar: 'hello',
  baz: 42,
})

const it = itObject[Symbol.iterator]()

it.next() // { value: [ 'foo', true ], done: false }
it.next() // { value: [ 'bar', 'hello' ], done: false }
it.next() // { value: [ 'baz', 42 ], done: false }
it.next() // { value: undefined, done: true }

Definitely useful for more complex, fine applications. But without digressing, let's stick to the for...of:

const itObject = iteratify({
  foo: true,
  bar: 'hello',
  baz: 42,
})

typeof itObject[Symbol.iterator] === 'function' // returns true, thus is iterable

for (let entry of itObject) {
  console.log(entry) // each returns relative entry
  // [ 'foo', true ]
  // [ 'bar', 'string' ]
  // [ 'baz', 42 ]
}

Conclusion

I hope the simplicity of the example served more as a gentle introduction to the subject rather than a source of yawning.

Here is the recap of some considerations.

  1. JavaScript built-in features like for...of call the method under the Symbol.iterator label
  2. Make the method it is to enumerate... unenumerable
  3. The next method can access and interact with the variables declared in the iterator (Closure) - you can do very cool things, not just keep track of an i counter ;)

Docs and Correlated

Iterators in detail (MUST READ, TRUST ME): You Don't Know JS: ES6 & Beyond

Originally posted on my GitHub

Contacts:

26