Iterables and for-of

Closely related to iterators, an iterable is an object with a Symbol.iterator property. The well-known Symbol.iterator symbol specifies a function that returns an iterator for the given object. All collection objects (arrays, sets, and maps) and strings are iterables in ECMAScript 6 and so they have a default iterator specified. Iterables are designed to be used with a new addition to ECMAScript: the for-of loop.

I> All iterators created by generators are also iterables, as generators assign the Symbol.iterator property by default.

At the beginning of this chapter, I mentioned the problem of tracking an index inside a for loop. Iterators are the first part of the solution to that problem. The for-of loop is the second part: it removes the need to track an index into a collection entirely, leaving you free to focus on working with the contents of the collection.

A for-of loop works on an iterable and uses its Symbol.iterator property to retrieve an iterator. Then, the for-of loop calls next() on that iterator each time the loop executes and stores the value from the result object in a variable. The loop continues this process until the returned object’s done property is true. Here’s an example:

  1. let values = [1, 2, 3];
  2. for (let num of values) {
  3. console.log(num);
  4. }

This code outputs the following:

  1. 1
  2. 2
  3. 3

This for-of loop first calls the Symbol.iterator method on the values array to retrieve an iterator. (The call to Symbol.iterator happens behind the scenes in the JavaScript engine itself.) Then iterator.next() is called, and the value property on the iterator’s result object is read into num. The num variable is first 1, then 2, and finally 3. When done on the result object is true, the loop exits, so num is never assigned the value of undefined.

If you are simply iterating over values in an array or collection, then it’s a good idea to use a for-of loop instead of a for loop. The for-of loop is generally less error-prone because there are fewer conditions to keep track of. Save the traditional for loop for more complex control conditions.

W> The for-of statement will throw an error when used on, a non-iterable object, null, or undefined.

Accessing the Default Iterator

You can use Symbol.iterator to access the default iterator for an object, like this:

  1. let values = [1, 2, 3];
  2. let iterator = values[Symbol.iterator]();
  3. console.log(iterator.next()); // "{ value: 1, done: false }"
  4. console.log(iterator.next()); // "{ value: 2, done: false }"
  5. console.log(iterator.next()); // "{ value: 3, done: false }"
  6. console.log(iterator.next()); // "{ value: undefined, done: true }"

This code gets the default iterator for values and uses that to iterate over the items in the array. This is the same process that happens behind-the-scenes when using a for-of loop.

Since Symbol.iterator specifies the default iterator, you can use it to detect whether an object is iterable as follows:

  1. function isIterable(object) {
  2. return typeof object[Symbol.iterator] === "function";
  3. }
  4. console.log(isIterable([1, 2, 3])); // true
  5. console.log(isIterable("Hello")); // true
  6. console.log(isIterable(new Map())); // true
  7. console.log(isIterable(new Set())); // true
  8. console.log(isIterable(new WeakMap())); // false
  9. console.log(isIterable(new WeakSet())); // false

The isIterable() function simply checks to see if a default iterator exists on the object and is a function. The for-of loop does a similar check before executing.

So far, the examples in this section have shown ways to use Symbol.iterator with built-in iterable types, but you can also use the Symbol.iterator property to create your own iterables.

Creating Iterables

Developer-defined objects are not iterable by default, but you can make them iterable by creating a Symbol.iterator property containing a generator. For example:

  1. let collection = {
  2. items: [],
  3. *[Symbol.iterator]() {
  4. for (let item of this.items) {
  5. yield item;
  6. }
  7. }
  8. };
  9. collection.items.push(1);
  10. collection.items.push(2);
  11. collection.items.push(3);
  12. for (let x of collection) {
  13. console.log(x);
  14. }

This code outputs the following:

  1. 1
  2. 2
  3. 3

First, the example defines a default iterator for an object called collection. The default iterator is created by the Symbol.iterator method, which is a generator (note the star still comes before the name). The generator then uses a for-of loop to iterate over the values in this.items and uses yield to return each one. Instead of manually iterating to define values for the default iterator of collection to return, the collection object relies on the default iterator of this.items to do the work.

I> “Delegating Generators” later in this chapter describes a different approach to using the iterator of another object.

Now you’ve seen some uses for the default array iterator, but there are many more iterators built in to ECMAScript 6 to make working with collections of data easy.