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 data structure referenced after the
of
keyword must be convertible to an iterable object or already be an iterable object. If this isn't possible, an error is thrownTypeError: <reference> is not iterable
. - The
for of
statement implicitly calls thenext()
method -- available on all iterable objects -- on every iteration, to move from element to element in an iterable object.
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:
jsFramework.entries()
.- Produces an iterable object with all the entries in the map, that is, all key-value elements in the map.jsFramework.keys()
.- Produces an iterable object with all the keys associated with the entries in the map.jsFramework.values()
.- Produces an iterable object with all the values associated with the entries in the map.jsFramework[Symbol.iterator]()
.- Produces an iterable object with all the entries in the map, which actually defaults to theMap
entries()
method. This symbol syntax technique is the one used byfor of
statements to create an iterable object from data types that support iterables.
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.