Iteratation Protocol (Concept)

The Iteration Protocol was introduced in ES6 to create iterable data structures. It enables the use of the for...of loop, spread syntax, and array destructuring assignment for data structures that adhere to the Iteration Protocol.

Iterable Protocol

The Iterable Protocol is a convention where an object conforms to the protocol by implementing a method with the well-known Symbol Symbol.iterator as its property key. This method, either directly implemented or inherited through the prototype chain, should return an iterator that adheres to the Iteration Protocol upon calling Symbol.iterator.

Example: Checking for iterability

const isIterable = v => v !== null && typeof v[Symbol.iterator] === 'function';

isIterable([]); // -> true
isIterable(''); // -> true
isIterable(new Map()); // -> true
isIterable(new Set()); // -> true
isIterable({}); // -> false

Iterator Protocol

The Iterator Protocol is a convention where an iterator adheres to the protocol by being returned when the Symbol.iterator method of an iterable is called. An iterator should have a next method and return an iterator result object with value and done properties.

Note: Objects that do not implement or inherit the Symbol.iterator method directly are not iterable since calling Symbol.iterator on them returns nothing.

Built-in Iterables

Built-in IterableSymbol.iterator Method
ArrayArray.prototype[Symbol.iterator]
StringString.prototype[Symbol.iterator]
MapMap.prototype[Symbol.iterator]
SetSet.prototype[Symbol.iterator]
TypedArrayTypedArray.prototype[Symbol.iterator]
argumentsarguments.prototype[Symbol.iterator]
DOM CollectionNodeList.prototype[Symbol.iterator] HTMLCollection.prototype[Symbol.iterator]

Differences between for...of and for...in

The for...of loop iterates over iterable objects (those with a Symbol.iterator method) and assigns the iterable's elements to a variable.

The for...in loop, on the other hand, iterates over all enumerable properties of an object's prototype chain, excluding properties with Symbol keys.

Iterable and Array-like Objects

Array-like objects are objects that resemble arrays, allowing access to property values using indices and possessing a length property. However, array-like objects are not iterable if they lack a Symbol.iterator method.

Examples of array-like objects:

const arrayLike = {
    0: 1,
    1: 2,
    2: 3,
    length: 3
};

for (let i = 0; i < arrayLike.length; i++) {
    console.log(arrayLike[i]); // 1, 2, 3
}

Note: arguments, NodeList, and HTMLCollection are both array-like objects and iterable objects.

Importance of Iteration Protocol

The Iteration Protocol establishes a convention for iterable data structures, offering a standardized interface for developers using constructs like for...of, spread syntax, and array destructuring assignment. It simplifies the process of accessing data elements, eliminating the need for developers to manually handle data traversal.

User-defined Iterables

Implementing User-defined Iterables

Objects that do not inherently adhere to the Iteration Protocol can be made iterable by following the protocol.

  • Implement the Symbol.iterator method.

  • Symbol.iterator should return an iterator with a next method.

  • The next method should return an iterator result object with value and done properties.

Example:

const evenNumbers = {
  [Symbol.iterator]() {
    let current = 0;
    const max = 10;

    return {
      next() {
        current += 2;
        return { value: current, done: current >= max };
      }
    };
  }
};

for (const num of evenNumbers) {
  console.log(num); // 2 4 6 8 10
}

Function Generating an Iterable

const evenNumbers = (max) => {
  let current = 0;

  return {
    [Symbol.iterator]() {
      return {
        next() {
          current += 2;
          return { value: current, done: current >= max };
        }
      };
    }
  };
};

const iterable = evenNumbers(10);

const iterator = iterable[Symbol.iterator]();

console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 4, done: false }
// ...

Function Generating an Iterable and Iterator

const evenNumbers = (max) => {
  let current = 0;

  return {
    [Symbol.iterator]() { return this; },
    next() {
       current += 2;
       return { value: current, done: current >= max };
    }
  };
}

const iterable = evenNumbers(10);

for (const num of iterable) {
  console.log(num); // 2 4 6 8 10
}

console.log(iterator.next()); // { value: 2, done: false }
// ...

Infinite Iterables and Lazy Evaluation

Lazy evaluation is a technique where data is not generated until it is required. Infinite iterables, as seen in the evenNumbers example, showcase lazy evaluation by only generating data when needed, avoiding unnecessary data creation.

const evenNumbers = () => {
  let current = 0;

  return {
    [Symbol.iterator]() { return this; },
    next() {
      current += 2;
      return { value: current, done: current >= max };
    }
  };
}

const iterable = evenNumbers();

for (const num of iterable) {
  if (num > 10000) break;
  console.log(num); // 2 4 6 8 10 ... 9998 10000
}

const [f1, f2, f3] = evenNumbers();
console.log(f1, f2, f3); // 2 4 6

The evenNumbers function creates an infinite iterable mechanism, but the data is not generated until needed. This allows for efficient execution, avoiding unnecessary memory consumption and supporting the representation of infinite values.