CHATTBLOG

Javascript Iterables

Technology
image
Javascript provides the for...of loop to be able to iterate through a list of items. This is applicable for not just Arrays and Sets but other data types like Maps. It is simple to visualize a looping solution for Arrays and Sets because they are lists of items, but it becomes less clear for Maps. Do we want to iterate on the keys or values or both?
We know when Maps are iterated on with for...of loops, we get an array [key, value] for each key value pair. So there must be some logic that decides how iteration should work for the case of Maps.
  const myMap = new Map([['a', 'value of a']]);
  for (const [key, value] of myMap) {
     console.log(`Key is "${key}" and its value is "${value}"`);
  };
  // Log: Key is "a" and its value is "value of a"
There might be other objects that we may want to iterate on with the help of for...of loops, but the unlike Maps the language does not know by itself how to iterate on them. The iterable protocol is a standared way to define how to iterate on an object. Once implemented, the object becomes an Iterable thus making it usable in for...of loops.

The Iterable Protocol

For an object to be iterable it must have a property with the key [Symbol.iterator]. The value of this key must be a function that returns the iterator object that is used to return the values of the iterable.
[Symbol.iterator]
This is a function that is called with no arguments when used the iterable is used in the for...of loop. It must return the iterator object.

The Iterator Object

For an object to be an iterator it must implement the following property so that the values of the itration can be retrieved.
next()
This is a function that the consumer would keep calling and every time it must return the next data object as required by the iteration. The data object returned on each call to the next() function should have the following properties:
value
This is the value of the current iteration.
done
This is a boolean value that should indicate that the end of the iterations has been reached and calling the next function of the iterable any further will not yeild any values.

Execution flow

Once an iterable is available, it is executed in a for...of loop in the following way.
Using an iterable in the for...of loop leads to its [Symbol.iterable] property to be retrieved and called. The for...of loop expectes then to get the iterator object, ie it must have a next property which is a function.
  const myMap = new Map([['a', 'value of a'], ['b', 'value of b']]);
  for (const [key, value] of myMap) { // executes myMap[Symbol.iterator]() and expects an iterator
     console.log(`Key is "${key}" and its value is "${value}"`);
  };
Once the iterator is recieved, the next() function is called and the value property of the data object received is set to the loop variable. When the block finishes executing, the next() function is again called to get the next data object.
  const myMap = new Map([['a', 'value of a'], ['b', 'value of b']]);
  for (const [key, value] of myMap) {
     // The next() function is called after each iteration
     console.log(`Key is "${key}" and its value is "${value}"`);
  };
This execution and subsequent calls to the next() function keeps repeating until the done property of the data object indicates that there is no more value to be recieved, so the loop can exit.
  const myMap = new Map([['a', 'value of a'], ['b', 'value of b']]);
  for (const [key, value] of myMap) {
     // for...of loop exits when the value of 'done' property is true for the data object received on calling the next() function
     console.log(`Key is "${key}" and its value is "${value}"`);
  };

Example Implementation

Since we now know all about what the protocols that define an iterable and how it is used, we can proceed to implement an iterable.
The Object.entries() function built into the Javascript Object prototype takes in an object as an argument and returns an iterable that iterates over each of the own enumerable keys and each iteration receives a key value pair in an array.
  const myObj = {a: 'value of a'};
  for (const [key, value] of Object.entries(myObj)) {
     console.log(`Key is "${key}" and its value is "${value}"`);
  };
  // Log: Key is "a" and its value is "value of a"
A custom re-implementation of the Object.entries() function: objectEntries, will give a clear insight about creating iterables.

The Iterable creator function

The first thing to note is that the objectEntries() function is not an iterable in itself, rather it creates an Iterable. When called with the subject object passed as an argument, it should return the Iterable object.
  const objectEntries = (obj) => {
     // return an Iterable
     return {
         [Symbol.iterator]: () => {
             // The object returned here should be of the iterator type, this will be implemented next
             return {};
         }
     }
  }

The iterator

Now that we have the iterable, calling the [Symbol.iterator] property should return an iterator.
  const objectEntries = (obj) => {
     return {
         [Symbol.iterator]: () => {
             // Create and return the iterator object
             return {
                 next: () => {
                     return {};
                 }
             }
         }
     }
  }
The next() function attached to the iterator object should return an iterator i.e. it must have a next function.
  const objectEntries = (obj) => {
 
     function getNextData() {
         return {
             value: undefined,
             done: false,
         }
     }
 
     return {
         [Symbol.iterator]: () => {
             return {
                 next: getNextData,
             }
         }
     }
  }
Now we have a complete iterable creator, when the objectEntries() function is called, it returns an iterable. However, this is of no use because the returned data has the value property equal to undefined and done property equal to false forever. So if it is used in a for...of loop, the iterations will never terminate. Now using the obj argument passed to the iterable creator meaningful values can be returned.
  const objectEntries = (obj) => {
     const keys = Object.keys(obj);
     let keyIndex = 0;
 
     function getNextData() {
         const done = keyIndex >= keys.length;
         const key = done ? undefined : keys[keyIndex++];
         return {
             value: [key, obj[key]],
             done,
         }
     }
 
     return {
         [Symbol.iterator]: () => {
 
             // Every time the iterable is used in a for...of loop, the key index is reset to 0
             keyIndex = 0;
 
             return {
                 next: getNextData,
             };
         }
     }
  }
With the help of the array of keys for the obj argument, along with the keyIndex accessible to the getIterator function by closure, we are able to return objects incrementing the keyIndex every time.
Using the iterable creator function will now allow us to use the returned iterable in a for...of loop.
  const myObj = {a: 'value of a', b: 'value of b'};
  for(const [key, value] of objectEntries(myObj)) {
     console.log(`Key is "${key}" and its value is "${value}"`);
  }
  // Logs:
  // Key is "a" and its value is "value of a"
  // Key is "b" and its value is "value of b"

Summary

  • Iterables are special objects that implement a [Symbol.iterator] property, which is a function.
  • Using this function for...of loops are able to determine the value of each iteration over the object.
  • The [Symbol.iterator] function must return an object that has a next property, which is a function.
  • By calling the next function repeatedly the values of the iteratable are received.
  • The value property of the object returned by calling the next function must hold the value of that iteration.
  • The done property of the object returned by calling the next function must indicate whether there are any more values to be retrieved.
author image

Sujoy Chatterjee

Author