What makes JavaScript Iterables Iterable?

Introduction

Have you wondered why arrays can be iterated (for...of) but objects cannot? JavaScript Iterables rely on the Iterator Protocol to implement iterable data structures.

As long as a data structure implements this protocol, it can become an Iterable, manipulable by for...of, spread operators, Array.from(), or destructuring assignment syntax.

The concept based on the Iterator Protocol extends to knowledge related to Generator Functions, enabling the creation of more specialized data structures.

Iterables

The so-called “iterable” refers to objects that implement the built-in special property Symbol.iterator and return an “iterator”. For example, arrays are inherently iterable:

const arr = [1, 2, 3];
console.log(typeof arr[Symbol.iterator]); // 'function'

Iterator

An Iterator must proceed to the next iteration through next, and each call of next() returns an object { value, done }, representing the current value and whether it’s finished:

const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

Example

You can define an iterable object yourself, such as a “range generator”:

const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let current = this.from;
const end = this.to;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
} else {
return { done: true };
}
},
};
},
};
for (const num of range) {
console.log(num); // 1, 2, 3
}

Generator: A More Concise Way to Implement an Iterator

While the above range works, it looks a bit cumbersome. Generator functions provide a more intuitive way to create iterators.

function* range(from, to) {
for (let i = from; i <= to; i++) {
yield i;
}
}
for (const num of range(1, 3)) {
console.log(num); // 1, 2, 3
}

Generators are a special type of function that do not execute their internal code immediately; they can “pause” and “resume” during execution. Each time yield is executed, it returns a result structured as { value, done }, rewriting the previous example:

function* threeStepGenerator() {
yield 1;
yield 2;
yield 3;
console.log('End');
}
const gen = threeStepGenerator();
console.log('Generator created');
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

Summary

  • Iterable: An object that has a Symbol.iterator method.
  • Symbol.iterator: An object that has a next() method and can gradually return { value, done }.
  • Generator: Defined with function*, using yield internally to produce values, essentially automating the creation of an Iterator.

In practice, concepts of lazy evaluation, asynchronous control, data stream generation, coroutine simulation, and iterator encapsulation can all apply relevantly.

Further Reading