Iterating over a collection of data is an important part of programming. Prior to ES2015, JavaScript provided statements such as for
, for...in
, and while
, and methods such as map()
, filter()
, and forEach()
for this purpose. To enable programmers to process the elements in a collection one at a time, ES2015 introduced the iterator interface.
An object is iterable if it has a Symbol.iterator
property. In ES2015, strings and collections objects such as Set
, Map
, and Array
come with a Symbol.iterator
property and thus are iterable. The following code gives an example of how to access the elements of an iterable one at a time:
const arr = [10, 20, 30];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // → {value: 10, done: false}
console.log(iterator.next()); // → {value: 20, done: false}
console.log(iterator.next()); // → {value: 30, done: false}
console.log(iterator.next()); // → {value: undefined, done: true}
Symbol.iterator
is a well-known symbol specifying a function that returns an iterator. The primary way to interact with an iterator is the next()
method. This method returns an object with two properties: value
and done
. The value
property contains the value of the next element in the collection. The done
property contains either true
or false
denoting whether or not the end of the collection has reached.
By default, a plain object is not iterable, but it can become iterable if you define a Symbol.iterator
property on it, as in this example:
const collection = {
a: 10,
b: 20,
c: 30,
[Symbol.iterator]() {
const values = Object.keys(this);
let i = 0;
return {
next: () => {
return {
value: this[values[i++]],
done: i > values.length
}
}
};
}
};
const iterator = collection[Symbol.iterator]();
console.log(iterator.next()); // → {value: 10, done: false}
console.log(iterator.next()); // → {value: 20, done: false}
console.log(iterator.next()); // → {value: 30, done: false}
console.log(iterator.next()); // → {value: undefined, done: true}
This object is iterable because it defines a Symbol.iterator
property. The iterator uses the Object.keys()
method to get an array of the object's property names and then assigns it to the values
constant. It also defines a counter variable and gives it an initial value of 0. When the iterator is executed it returns an object that contains a next()
method. Each time the next()
method is called, it returns a {value, done}
pair, with value
holding the next element in the collection and done
holding a Boolean indicating if the iterator has reached the need of the collection.
While this code works perfectly, it’s unnecessarily complicated. Fortunately, using a generator function can considerably simplify the process:
const collection = {
a: 10,
b: 20,
c: 30,
[Symbol.iterator]: function * () {
for (let key in this) {
yield this[key];
}
}
};
const iterator = collection[Symbol.iterator]();
console.log(iterator.next()); // → {value: 10, done: false}
console.log(iterator.next()); // → {value: 20, done: false}
console.log(iterator.next()); // → {value: 30, done: false}
console.log(iterator.next()); // → {value: undefined, done: true}
Inside this generator, a for...in
loop is used to enumerate over the collection and yield the value of each property. The result is exactly the same as the previous example, but it’s greatly shorter.
A downside of iterators is that they are not suitable for representing asynchronous data sources. ES2018’s solution to remedy that is asynchronous iterators and asynchronous iterables. An asynchronous iterator differs from a conventional iterator in that, instead of returning a plain object in the form of {value, done}
, it returns a promise that fulfills to {value, done}
. An asynchronous iterable defines a Symbol.asyncIterator
method (instead of Symbol.iterator
) that returns an asynchronous iterator.
An example should make this clearer:
const collection = {
a: 10,
b: 20,
c: 30,
[Symbol.asyncIterator]() {
const values = Object.keys(this);
let i = 0;
return {
next: () => {
return Promise.resolve({
value: this[values[i++]],
done: i > values.length
});
}
};
}
};
const iterator = collection[Symbol.asyncIterator]();
console.log(iterator.next().then(result => {
console.log(result); // → {value: 10, done: false}
}));
console.log(iterator.next().then(result => {
console.log(result); // → {value: 20, done: false}
}));
console.log(iterator.next().then(result => {
console.log(result); // → {value: 30, done: false}
}));
console.log(iterator.next().then(result => {
console.log(result); // → {value: undefined, done: true}
}));
Note that it’s not possible to use an iterator of promises to achieve the same result. Although a normal, synchronous iterator can asynchronously determine the values, it still needs to determine the state of "done" synchronously.
Again, you can simplify the process by using a generator function, as shown below:
const collection = {
a: 10,
b: 20,
c: 30,
[Symbol.asyncIterator]: async function * () {
for (let key in this) {
yield this[key];
}
}
};
const iterator = collection[Symbol.asyncIterator]();
console.log(iterator.next().then(result => {
console.log(result); // → {value: 10, done: false}
}));
console.log(iterator.next().then(result => {
console.log(result); // → {value: 20, done: false}
}));
console.log(iterator.next().then(result => {
console.log(result); // → {value: 30, done: false}
}));
console.log(iterator.next().then(result => {
console.log(result); // → {value: undefined, done: true}
}));
Normally, a generator function returns a generator object with a next()
method. When next()
is called it returns a {value, done}
pair whose value
property holds the yielded value. An async generator does the same thing except that it returns a promise that fulfills to {value, done}
.
An easy way to iterate over an iterable object is to use the for...of
statement, but for...of
doesn't work with async iterables as value
and done
are not determined synchronously. For this reason, ES2018 provides the for...await...of
statement. Let’s look at an example:
const collection = {
a: 10,
b: 20,
c: 30,
[Symbol.asyncIterator]: async function * () {
for (let key in this) {
yield this[key];
}
}
};
(async function () {
for await (const x of collection) {
console.log(x);
}
})();
// logs:
// → 10
// → 20
// → 30
In this code, the for...await...of
statement implicitly calls the Symbol.asyncIterator
method on the collection object to get an async iterator. Each time through the loop, the next()
method of the iterator is called, which returns a promise. Once the promise is resolved, the value
property of the resulting object is read to the x
variable. The loop continues until the done
property of the returned object has a value of true
.
Keep in mind that the for...await...of
statement is only valid within async generators and async functions. Violating this rule results in a SyntaxError
.
The next()
method may return a promise that rejects. To gracefully handle a rejected promise, you can wrap the for...await...of
statement in a try...catch
statement, like this:
const collection = {
[Symbol.asyncIterator]() {
return {
next: () => {
return Promise.reject(new Error('Something went wrong.'))
}
};
}
};
(async function() {
try {
for await (const value of collection) {}
} catch (error) {
console.log('Caught: ' + error.message);
}
})();
// logs:
// → Caught: Something went wrong.
Chrome | Firefox | Safari | Edge |
---|---|---|---|
63 | 57 | 12 | No |
Chrome Android | Firefox Android | iOS Safari | Edge Mobile | Samsung Internet | Android Webview |
---|---|---|---|---|---|
63 | 57 | 12 | No | 8.2 | 63 |
Node.js:
- 8.10.0 (requires the --harmony_async_iteration flag)
- 10.0.0 (full support)