for of statements in JS

Iterable & iterator protocols and iterable objects  

The iterable and iterator protocols are directly related to loops made with the for of statement. In fact, the for of statment doesn't work unless it's with JavaScript objects that support the iterable and iterator protocols. The good news is that starting from ECMAScript 6 (ES2015), almost all JavaScript data types can produce objects that support the iterable and iterator protocols, objects that are also known as iterable objects.

So before you completely forget about the for, while, do while and for in loop syntax techniques and drop them in favor of for of statements, it's very important to understand for of statements only work with data types that generate iterable objects.

Let's start by looking at the for of statement syntax shown in listing 6-6.

Listing 6-6. for of statement syntax
for (let <reference> of <iterable_object>) {
    console.log(<reference>);
}

As you can see listing 6-6, a loop with the for of statement consists of an expression wrapped in () and prefixed with for. The expression consists of a variable reference to hold each element of an iterable object on every iteration. The block statement in curly brackets {} is declared to execute business logic on each iteration which has access to the variable reference declared in between the for and of keywords.

An iterable object can be as simple as a string (e.g. to get each letter in a string), an array (e.g. to get each element in an array), to something as elaborate as a data structure representing a file (e.g. to get each line in a file). All an iterable object needs to comply with are the iterable and iterator protocols, but more details on this shortly.

Listing 6-7 illustrates how to create loops with the for of statement and various data types that produce iterable objects.

Listing 6-7. For loops with for of statements
let language = "JavaScript";
let primeNumbers = [2,3,5,7,11];
let jsFramework =  new Map([["name","React"],["creator","Facebook"],["purpose","UIs"]])

// Loop over String
for (let value of language) {
    console.log(value);
}

// Loop over Array
for (let value of primeNumbers) {
    console.log(value);
}

// Loop over Map
for (let [key, value] of jsFramework){
     console.log(`langauge 
  key: ${key};
  value: ${value}`);
}

The examples in listing 6-7 perform a loop over a String data type, an Array data type and a Map data type using the for of statement to output each of its elements. Notice the third example uses a Map data type and the syntax for (let [key, value] of jsFramework) with two references -- one for key and another for values -- vs. a single reference like the first two examples, this is due to the way Map data types use key-value elements.

In most cases, a for of statement is used to walk through all the elements in an iterable object, but it's possible to customize or terminate a for of statement prematurely just like it's done in other JS loop statements, using the break or continue statement. Listing 6-8 illustrates how to prematurely end a for of loop.

Listing 6-8. For loops with for of statement and break and continue keywords
let language = "JavaScript";
let primeNumbers = [2,3,5,7,11];
let jsFramework =  new Map([["name","React"],["creator","Facebook"],["purpose","UIs"]])

// break statement is valid in for of statement
for (let value of language) {
     if (value == "S") { 
       break;
     }
    console.log(value);
}

// continue statement is valid in for of statement
for (let value of primeNumbers) {
     if (value > 4 && value < 10) {
     continue;
    }
    console.log(value);
}

// break statement is valid in a for of statement
for (let [key, value] of jsFramework){
      if (key == "creator" && value == "Facebook") {
        break;
      }
     console.log(`langauge 
  key: ${key};
  value: ${value}`);
}

The examples in listing 6-8 illustrate how it's possible to use break and continue statements to exit loops or iterations prematurely in a loop with a for of statement.

The examples in listing 6-7 and listing 6-8 all work with iterable objects, however, it's important to illustrate that loops with the for of statement don't work with non-iterable objects. Listing 6-9 illustrates an example with an Object data type that fails to work with the for of statement.

Listing 6-9. For loops with for of statement and non-iterable objects
let jsFramework =  {"name":"React","creator":"Facebook","purpose":"UIs"}

// Loop over object with for in, success!
for (let [key, value] in jsFramework){
     console.log(`langauge 
  key: ${key};
  value: ${value}`);
}

// Loop over object with for of: error TypeError: jsFramework is not iterable
for (let [key, value] of jsFramework){
     console.log(`langauge 
  key: ${key};
  value: ${value}`);
}

Listing 6-9 starts by declaring the jsFramework data structure as an Object object data type. The first loop makes use of the for in statement which works correctly on Object data types, however, notice the second loop that uses the for of statement throws the error TypeError: jsFramework is not iterable. This last error occurs because the Object data type doesn't produce iterable objects.

Iterable objects: Behind the scenes of the for of statement  

The for of statement has some "behind the scenes" behaviors that allow it to progress over elements in a data structure:

The first behavior is the reason why the String, Array and Map data structures in listing 6-7 and listing 6-8 work with for of statements, all these data types are convertible to iterable objects since they have an @@iterator method, where @@ denotes a Symbol data type (e.g. [Symbol.iterator]()). It's also the reason why an Object object data type doesn't work with a for of statement, since it isn't directly convertible to an iterable object because it doesn't have an @@iterator method, as shown in listing 6-9. Note the Object object data type does have certain methods to convert its elements to iterable objects, the details of which are provided shortly.

The second behavior is how a for of loop progresses from one element to another in an iterable object. Behind the scenes, the iterable object's next() method is called after each iteration. And it's this next() method that holds a powerful feature to manually advance over for loops. What if instead of letting the for of statement implicitly call the next() method on an iterable object, you could call it explicitly to have more control over the loop behavior ? That's entirely possible and is also the foundation of generators and yield expressions. But before we change topic, let's take a closer look at how to explicitly create iterable objects and use an iterator object's next() method.

Listing 6-10 illustrates how to loop over iterable objects and use the next() method to manually advance over a data structure as if it were a for loop.

Listing 6-10. Iterable objects and the next() method
let jsFramework =  new Map([["name","React"],["creator","Facebook"],["purpose","UIs"]]);

// jsFramework data type
console.log("jsFramework is: %s", Object.prototype.toString.call(jsFramework))
// Create iterable object from jsFramework
let jsFrameworkIterator = jsFramework[Symbol.iterator]();
// jsFrameworkIterator data type
console.log("jsFrameworkIterator is: %s", Object.prototype.toString.call(jsFrameworkIterator))
// Create iterable object from jsFramework with entries(), 
// identical results to [Symbol.iterator]
let jsFrameworkIteratorEntries = jsFramework.entries();
// jsFrameworkIteratorEntries data type
console.log("jsFrameworkIteratorEntries is: %s", Object.prototype.toString.call(jsFrameworkIteratorEntries))
// Create iterable object from jsFramework with keys()
let jsFrameworkIteratorKeys = jsFramework.keys();
// jsFrameworkIteratorKeys data type
console.log("jsFrameworkIteratorKeys is: %s", Object.prototype.toString.call(jsFrameworkIteratorKeys))
// Create iterable object from jsFramework with values()
let jsFrameworkIteratorValues = jsFramework.values();
// jsFrameworkIteratorValues data type
console.log("jsFrameworkIteratorValues is: %s", Object.prototype.toString.call(jsFrameworkIteratorValues))

// Manually move over iterable jsFrameworkIterator with next()
console.log(jsFrameworkIterator.next().value); // [ 'name', 'React' ]
console.log(jsFrameworkIterator.next().value); // [ 'creator', 'Facebook' ]
// Output full object of next()
console.log(jsFrameworkIterator.next()); // { value: [ 'purpose', 'UIs' ], done: false }
// Output return value of next() confirming iterable reached end
console.log(jsFrameworkIterator.next()); //  value: undefined, done: true }

// Manually move over iterable jsFrameworkIteratorEntries with next()
console.log(jsFrameworkIteratorEntries.next().value); // [ 'name', 'React' ]
console.log(jsFrameworkIteratorEntries.next().value); // [ 'creator', 'Facebook' ]
// Output full object of next()
console.log(jsFrameworkIteratorEntries.next()); // { value: [ 'purpose', 'UIs' ], done: false }
// Output return value of next() confirming iterable reached end
console.log(jsFrameworkIteratorEntries.next()); //  value: undefined, done: true }

// Manually move over iterable jsFrameworkIteratorKeys with next()
console.log(jsFrameworkIteratorKeys.next().value); // name
console.log(jsFrameworkIteratorKeys.next().value); // creator
// Output full object of next()
console.log(jsFrameworkIteratorKeys.next()); // { value: 'purpose', done: false }
// Output return value of next() confirming iterable reached end
console.log(jsFrameworkIteratorKeys.next()); // { value: undefined, done: true }


// Manually move over iterable jsFrameworkIteratorValues with next()
console.log(jsFrameworkIteratorValues.next().value); // React
console.log(jsFrameworkIteratorValues.next().value); // Facebook
// Output full object of next()
console.log(jsFrameworkIteratorValues.next()); // { value: 'UIs', done: false }
// Output return value of next() confirming iterable reached end
console.log(jsFrameworkIteratorValues.next()); // { value: undefined, done: true }

Listing 6-10 creates a map and assigns it to the jsFramework reference with a log statement that confirms it's an [object Map] data type. This means the jsFramework is not yet an iterable object, it can become an iterable object if placed in a for of statement, but then we wouldn't be able to use the next() method. The solution is to explicitly create an iterable object from the jsFramework map, for which there are various alternatives shown in listing 6-10:

A key takeaway of all these alternatives to create iterable objects is they all output a [object Map Iterator] data type, indicating they're iterables and can thus operate with the next() method. Equipped with various iterable objects, notice how the next() method is called on each iterable object and how on each call the iterable object moves forward to its next element.

The next() method returns an Object object with the done and value properties, where the former indicates if the iterable is done (i.e. there are no more iterable elements) and the latter returns the value of the current iteration. This is why value is chained to the next() method, to output the value of the current iteration. Toward the end of each iterable object, you can see the final call characteristics.next() outputs { value: undefined, done: true } which indicates the next value in the iterable is undefined and the iterable as a whole is done. This same mechanism is how for of statements determine when a loop finishes.

Iterable objects produced by data type methods: Things you can put through for of statements

This last section showcased how the Map data type has several methods to produce iterable objects, which in turn makes them candidates to either use the iterable object's next() method on them, or more practically, candidates to run through a for of statement.

And just like the Map data type has its own methods to produce iterable objects, most JavaScript data types also have built-in methods that produce iterable objects. Therefore, there are many alternatives to produce iterable object data structures that can be run through for of statements, beyond the basic examples presented in listing 6-7 which simply use standard data types that support the @@iterator method.

Inclusively, although the Object object data type with its ample and legacy baggage can't be automatically converted to an iterable object -- as shown in listing 6-9 -- throughout the years it has incorporated methods that allow it to be converted to an Array data type (e.g. Object.entries(), Object.keys(), Object.values()), which in turn allow an Object data type to be converted to an Array first, which can then be converted to an iterable object with Array methods that produce iterable objects.