Synchronous iteration is a protocol (interfaces plus rules for using them) that connects two groups of entities in JavaScript:
Data sources: On one hand, data comes in all shapes and sizes. In JavaScript’s standard library, we have the linear data structure Array, the ordered collection Set (elements are ordered by time of addition), the ordered dictionary Map (entries are ordered by time of addition), and more. In libraries, we may find tree-shaped data structures and more.
Data consumers: On the other hand, we have a whole class of constructs and algorithms that only need to access their input sequentially: one value at a time, until all values were visited. Examples include the for-of
loop and spreading into function calls (via ...
).
The iteration protocol connects these two groups via the interface Iterable
: data sources deliver their contents sequentially “through it”; data consumers get their input via it.
Figure 32.1 illustrates how iteration works: data consumers use the interface Iterable
; data sources implement it.
The JavaScript way of implementing interfaces
In JavaScript, an object implements an interface if it has all the methods that it describes. The interfaces mentioned in this chapter only exist in the ECMAScript specification.
Both sources and consumers of data profit from this arrangement:
If we develop a new data structure, we only need to implement Iterable
and a raft of tools can immediately be applied to it.
If we write code that uses iteration, it automatically works with many sources of data.
Two roles (described by interfaces) form the core of iteration (figure 32.2):
These are type definitions (in TypeScript’s notation) for the interfaces of the iteration protocol:
interface Iterable<T> {
[Symbol.iterator]() : Iterator<T>;
}
interface Iterator<T> {
next() : IteratorResult<T>;
}
interface IteratorResult<T> {
value: T;
done: boolean;
}
The interfaces are used as follows:
Iterable
for an iterator via the method whose key is Symbol.iterator
.
Iterator
returns the iterated values via its method .next()
.
.value
is the iterated value.
.done
indicates if the end of the iteration has been reached yet. It is true
after the last iterated value and false
beforehand.
This is an example of using the iteration protocol:
const iterable = ['a', 'b'];
// The iterable is a factory for iterators:
const iterator = iterable[Symbol.iterator]();
// Call .next() until .done is true:
assert.deepEqual(
iterator.next(), { value: 'a', done: false });
assert.deepEqual(
iterator.next(), { value: 'b', done: false });
assert.deepEqual(
iterator.next(), { value: undefined, done: true });
while
The following code demonstrates how to use a while
loop to iterate over an iterable:
function logAll(iterable) {
const iterator = iterable[Symbol.iterator]();
while (true) {
const {value, done} = iterator.next();
if (done) break;
console.log(value);
}
}
logAll(['a', 'b']);
Output:
a
b
Exercise: Using sync iteration manually
exercises/sync-iteration/sync_iteration_manually_exrc.mjs
We have seen how to use the iteration protocol manually, and it is relatively cumbersome. But the protocol is not meant to be used directly – it is meant to be used via higher-level language constructs built on top of it. This section shows what that looks like.
JavaScript’s Arrays are iterable. That enables us to use the for-of
loop:
const myArray = ['a', 'b', 'c'];
for (const x of myArray) {
console.log(x);
}
Output:
a
b
c
Destructuring via Array patterns (explained later) also uses iteration under the hood:
const [first, second] = myArray;
assert.equal(first, 'a');
assert.equal(second, 'b');
JavaScript’s Set data structure is iterable. That means for-of
works:
const mySet = new Set().add('a').add('b').add('c');
for (const x of mySet) {
console.log(x);
}
Output:
a
b
c
As does Array-destructuring:
const [first, second] = mySet;
assert.equal(first, 'a');
assert.equal(second, 'b');
Map.groupBy()
groups the items of an iterable into Map entries whose keys are provided by a callback:
assert.deepEqual(
Map.groupBy([0, -5, 3, -4, 8, 9], x => Math.sign(x)),
new Map().set(0, [0]).set(-1, [-5,-4]).set(1, [3,8,9])
);
The items to be grouped can come from any iterable:
function* generateNumbers() {
yield 2;
yield -7;
yield 4;
}
assert.deepEqual(
Map.groupBy(generateNumbers(), x => Math.sign(x)),
new Map().set(1, [2,4]).set(-1, [-7])
);
There is also Object.groupBy()
which produces an object instead of a Map:
assert.deepEqual(
Object.groupBy([0, -5, 3, -4, 8, 9], x => Math.sign(x)),
{'0': [0], '-1': [-5,-4], '1': [3,8,9], __proto__: null}
);
Map.groupBy()
and Object.groupBy()
.groupBy()
(see example later in this section)?
The Promise combinator Promise.allSettled()
returns Arrays such as the following one:
const settled = [
{ status: 'rejected', reason: 'Jhon' },
{ status: 'fulfilled', value: 'Jane' },
{ status: 'fulfilled', value: 'John' },
{ status: 'rejected', reason: 'Jaen' },
{ status: 'rejected', reason: 'Jnoh' },
];
We can group the Array elements as follows:
const {fulfilled, rejected} = Object.groupBy(settled, x => x.status); // (A)
// Handle fulfilled results
assert.deepEqual(
fulfilled,
[
{ status: 'fulfilled', value: 'Jane' },
{ status: 'fulfilled', value: 'John' },
]
);
// Handle rejected results
assert.deepEqual(
rejected,
[
{ status: 'rejected', reason: 'Jhon' },
{ status: 'rejected', reason: 'Jaen' },
{ status: 'rejected', reason: 'Jnoh' },
]
);
For this use case, Object.groupBy()
works better because we can use destructuring (line A).
In the next example, we’d like to group persons by country:
const persons = [
{ name: 'Louise', country: 'France' },
{ name: 'Felix', country: 'Germany' },
{ name: 'Ava', country: 'USA' },
{ name: 'Léo', country: 'France' },
{ name: 'Oliver', country: 'USA' },
{ name: 'Leni', country: 'Germany' },
];
assert.deepEqual(
Map.groupBy(persons, (person) => person.country),
new Map([
[
'France',
[
{ name: 'Louise', country: 'France' },
{ name: 'Léo', country: 'France' },
]
],
[
'Germany',
[
{ name: 'Felix', country: 'Germany' },
{ name: 'Leni', country: 'Germany' },
]
],
[
'USA',
[
{ name: 'Ava', country: 'USA' },
{ name: 'Oliver', country: 'USA' },
]
],
])
);
For this use case, Map.groupBy()
is a better choice because we can use arbitrary keys in Maps whereas in objects, keys are limited to strings and symbols.
The following built-in data structures are iterable:
To iterate over the properties of objects, we need helpers such as Object.keys()
and Object.entries()
. That is necessary because properties exist at a different level that is independent of the level of data structures.
This section lists constructs that use synchronous iteration.
Destructuring via an Array pattern:
const [x,y] = iterable;
Spreading (via ...
) into function calls and Array literals:
func(...iterable);
const arr = [...iterable];
The for-of
loop:
for (const x of iterable) { /*···*/ }
yield*
:
function* generatorFunction() {
yield* iterable;
}
const obj = Object.fromEntries(iterableOverKeyValuePairs);
const arr = Array.from(iterable);
new Map()
and new WeakMap()
:
const m = new Map(iterableOverKeyValuePairs);
const wm = new WeakMap(iterableOverKeyValuePairs);
new Set()
and new WeakSet()
:
const s = new Set(iterableOverElements);
const ws = new WeakSet(iterableOverElements);
Promise combinator functions: Promise.all()
etc.
const promise1 = Promise.all(iterableOverPromises);
const promise2 = Promise.race(iterableOverPromises);
const promise3 = Promise.any(iterableOverPromises);
const promise4 = Promise.allSettled(iterableOverPromises);