Iteration protocols in JavaScript
Iterations protocols allow us to loop over elements using a common interface. There are three main constructs that can be confusing sometimes:
Iterators
Iterators are any objects that implement the next method with the following semantics:
type Iterator<T> = {
next(): {
done: boolean;
value: T;
};
};
function idMaker(maxId) {
let index = 0;
return {
next() {
if (index > maxId) {
return { done: true };
}
return {
value: index++,
done: false,
};
},
};
}
To use an iterator:
const iterator = idMaker(10);
let result = iterator.next();
while (!result.done) {
console.log(result.value); // 0, 1, 2, ..
result = iterator.next();
}
Iterables
Iterables are objects that return an iterator (what was defined above).
type Iterable<T> = {
[Symbol.iterator](): Iterator<T>;
};
They are more general than iterators, in the following example IdMaker is a class with extra methods:
class IdMaker {
constructor(maxId) {
this.maxId = maxId;
}
label(id) {
return `ID: ${id}`;
}
// implementing the iterator() function that returns an iterator
[Symbol.iterator]() {
let index = 0;
const maxId = this.maxId;
return {
next() {
if (index > maxId) {
return { done: true };
}
return {
value: index++,
done: false,
};
},
};
}
}
We can use more looping constructs with iterables, for example the for/of, and the spread syntax:
const idMaker = new IdMaker(10);
for (const id of idMaker) {
console.log(id);
}
const idMaker = new IdMaker(10);
console.log([...idMaker]);
We can also access the iterator of an iterable directly to loop over results individually:
const idMaker = new IdMaker(10);
const iterator = idMaker[Symbol.iterator](); // we access the iterator first
let result = iterator.next();
while (!result.done) {
console.log(result.value); // 0, 1, 2, ..
result = iterator.next();
}
Note: Iterators can also become iterables by implementing the iterator() method:
function idMaker(maxId) {
let index = 0;
return {
next() {
if (index > maxId) {
return { done: true };
}
return {
value: index++,
done: false,
};
},
// here:
[Symbol.iterator]() {
return this;
},
};
}
Generators
Generators can also be used instead of iterators for a simpler syntax:
class IdMaker {
constructor(maxId) {
this.maxId = maxId;
}
label(id) {
return `ID: ${id}`;
}
// the iterator is a generator
*[Symbol.iterator]() {
for (let index = 0; index < this.maxId; index++) {
yield index;
}
}
}
To loop over the results, we can use the same constructs for iterables like for/of and spread syntax.