JavaScript for loops
JavaScript has many ways to loop over data structures, depending on what you plan to do with the data inside it, the size of a data structure, as well as the type of data strucutre. In this chapter you'll learn the various ways to loop over JavaScript data:
- The
for
statement: Explicit counters, iterations and step sizes, the early years - The
break
andcontinue
statements: Exiting for loops and iterations prematurely - Label statements in
for
loops: GoTo a specific point in the workflow - The
while
anddo while
statements: Loops that run with one expression - The
for in
statement: A loop for key-value data structures, the early years - The
Array
data type baked-in for loops, methods with callbacks:every()
,filter()
,forEach()
,map()
,reduce()
&reduceRight()
- The
Map
data type &Set
data type baked-in for loopforEach()
method with callback - The
for of
statement: Iterable and iterator protocols - Iterable objects and the
next()
method: Behind the scenes of thefor of
statement & manually advancing over iterables - Generators and yield expressions
The for
statement: Explicit counters, iterations and step sizes, the early years
The earliest JavaScript for loop you're likely to encounter to this day is one with a for
statement. The reason the for
statement is still in use today, is because it's the most flexible, with the ability to prematurely finish a loop when a condition is true, define the number of times a loop runs by something other than a data structure's size, as well as adjust the increments in which iterations are made (a.k.a. step size).
The syntax of the for
statement consists of three expressions separated by ;
wrapped in ()
and prefixed with for
, with the block of code to execute on each iteration wrapped in a block statement by curly brackets {}
. The purpose of the three expressions that follow the for
statement is the following:
- Initialization expression for the loop. Generally used to declare a loop control variable (e.g.
let i = 0;
). - Expression evaluated at the start of every loop iteration, to determine to execute the block statement when
true
or finish the loop whenfalse
. Generally used with control variable & limit to determine amount of iterations (e.g.i < 10;
). - Expression evaluated at the end of every loop iteration. Generally used to increase or decrease loop control variable (e.g.
i++;
)
Due to the flexibility of the for
statement, strictly speaking all three expressions that accompany a for
statement are optional parameters. However, although it's valid to declare a for loop in the form for(;;) { }
-- which is an endless loop -- and rely on a statement inside the block statement to terminate it, this type of for
syntax without expressions can be non-obvious and confusing, not to mention there are other more obvious JavaScript for loop syntax alternatives to declare endless loops, such as the the while
and do while
statements.
Although the expressions to control a for
statement can appear to be fail-safe, the manner in which these expressions are declared can have an impact on performance. This impact varies when declaring the control variable with the var
keyword or the let
keyword -- because it influences block scope visibility and hoisting -- as well as if the number of iterations are specified as a constant or calculated on every iteration.
Listing 6-1 illustrates four variations of this classic JavaScript for loop syntax with performance metrics using the console
object's console.time()
& console.timeEnd()
methods.
Listing 6-1. For loops with for
statements and performance metrics
let primeNumbers = [2,3,5,7,11]; // Option 1) Least efficient // var as counter & size in loop definition console.time("loopOption1"); for (var i = 0; i < primeNumbers.length; i++) { console.log(primeNumbers[i]); } console.timeEnd("loopOption1"); // Option 2) More efficient // var predefined with counter & size var primeLength = primeNumbers.length; console.time("loopOption2"); for (var j = 0; j < primeLength; j++) { console.log(primeNumbers[j]); } console.timeEnd("loopOption2"); // Option 3) More efficient // let as count & size in loop definition console.time("loopOption3"); for (let k = 0; k < primeNumbers.length; k++) { console.log(primeNumbers[k]); } console.timeEnd("loopOption3"); // Option 4) Most efficient // let as count & size in predefined const const primeLengthConst = primeNumbers.length; console.time("loopOption4"); for (let l = 0; l < primeLengthConst; l++) { console.log(primeNumbers[l]); } console.timeEnd("loopOption4");
The first option and least efficient syntax presented in listing 6-1 consists of declaring the control variable with the var
keyword and declaring the number of times the loop runs directly with the .length
property of the data structure. The problem with this first option is due to block scope & hoisting it leads to the repeated calculation of a constant value. In this case, the i
for loop control variable is declared inline, which forces the JavaScript engine to hoist the variable. In addition, an evaluation is made against primeNumbers.length
on every iteration to determine when to finish the loop, something that's inefficient for a value that doesn't change over the course of a loop.
The second option in listing 6-1 produces a slightly more efficient for loop by declaring the number of iterations in a predefined variable primeLength
vs. calling the .length
property on the data structure on every iteration. In the third option, the loop is executed even more efficiently by declaring the counter variable with the let
keyword -- to avoid hoisting -- although it still uses the .length
property on the data structure on every iteration.
The fourth option in listing 6-1 which produces the most efficient for loop, uses the let
keyword to declare the control variable and declares the number of times the loop runs in a predefined variable primeLengthConst
as a const
vs. calling the .length
property on the data structure on every iteration. As you can see from these examples, the biggest performance impact in this kind of for loop is in the control variable delivering more performance with the let
keyword vs. var
, followed by declaring the number of iterations in a predefined variable with const
vs. calculating the number of iterations every time with the .length
property of a data structure.
The break
and continue
statements: Exiting for loops and iterations prematurely
Often times a loop is executed until a certain condition evaluates to true, exiting its workload prematurely. In JavaScript, the continue
statement is used to exit a foor loop iteration before it concludes, whereas the break
statement is used to exit a for loop entirely before it concludes. It's worth pointing out the break
and continue
statements are applicable to any JavaScript type of loop and not just loops created with the for
statement presented earlier.
Listing 6-2 illustrates the use of the break
and continue
statements in for loops.
Listing 6-2. For loops with break
and continue
statements
let primeNumbers = [2,3,5,7,11]; const primeLength = primeNumbers.length; for (let i = 0; i < primeLength; i++) { // Continue to next iteration when primeNumber > 4 & < 10 if (primeNumbers[i] > 4 && primeNumbers[i] < 10) { continue; } console.log(primeNumbers[i]); } let vowels = ["a","e","i","o","u"]; const vowelsLength = vowels.length; console.log("vowels ",vowelsLength); for (let j = vowelsLength-1; j >= 0; j--) { // Break loop when vowel == "i" if (vowels[j] == "i") { break; } console.log(vowels[j]); }
The first example in listing 6-2 loops over the primeNumbers
array and inside the block of code checks if the value of the iteration on primeNumbers
is larger than 4
or lower than 10
to invoke a continue
statement. In this case, when a primeNumbers
value is between 4
and 10
the continue
statement causes the loop to immediately move to the next iteration, skipping the log statement when the primeNumbers
iteration value is 5
and 7
.
The second example in listing 6-2 loops over the vowels
array with a decreasing counter to facilitate a reverse order loop. Inside the block of code, notice a check is made for when the value of the iteration on vowels
equals "i"
to invoke a break
statement. In this case, when a vowels
value equals "i"
, the condition evaluates to true and the break
statement causes the loop to finish immediately, skipping the log statement when the vowels
iteration value equals "i"
, as well as any remaining iterations (i.e. when a vowels
value is "e"
and "a"
).
Label statements in for loops: GoTo a specific point in the workflow
JavaScript labels support the GoTo[1] programming construct. Although GoTo's are often controversial in many programming languages since they can reduce readability and lead to "spaghetti" workflows, GoTo's serve to change the execution workflow of a program to a predetermined line of code.
In the context of for loops, labels can be particularly helpful to change the execution workflow in nested loops. While the break
and continue
statements are used to completely exit a loop or prematurely exit a loop iteration, they do so in the context of a single loop. In order to exit loop iterations in either form when more than one loop is involved, label statements are used in conjunction with the break
and continue
statements to indicate to what line of code to break or continue a workflow.
Label names in JavaScript are declared as words followed by a colon :
to indicate a potential location where workflow control can be sent, which means they're typically declared right before the start of a for
loop. In order to send workflow control to a given label, the break
and continue
statements are used in conjunction with a label's name, where a break
or continue
statement is followed by a label name. It's worth pointing out label statements are applicable to any JavaScript type of loop and not just loops created with the for
statement presented earlier.
Listing 6-3 illustrates the use of labels in for loops.
Listing 6-3. For loops with labels
let primeNumbers = [2,3,5,7,11]; const primeLength = primeNumbers.length; // Outer label xPlane: for (let x = 0; x < primeLength; x++) { // Inner label yPlane: for (let y = 0; y < primeLength; y++) { console.log("Processing ", primeNumbers[x], primeNumbers[y]); if (primeNumbers[x] == primeNumbers[y]) { console.log("Symmetrical coordinate ", primeNumbers[x], primeNumbers[y]); // Continue to outer label after symmetrical match continue xPlane; } } } let vowels = ["a","e","i","o","u"]; const vowelsLength = vowels.length; // Placeholder array to use inside loop let diphthongs = []; // Outer loop outerLoop: for (let i = 0; i < vowelsLength; i++) { // Inner loop innerLoop: for (let j = 0; j < vowelsLength; j++) { // Continue to inner loop if vowels are the same if (vowels[i] == vowels[j]) { continue innerLoop; } // Add up to ten diphthongs to diphthongs array if (diphthongs.length < 10) { diphthongs.push(`${vowels[i]}${vowels[j]}`); // Break to outer label after ten diphthongs } else { break outerLoop; } } } // Output all diphthongs added to diphthongs array console.log("First ten diphthongs ", diphthongs);
The first example in listing 6-3 creates a pair of loops to iterate over the primeNumbers
array to output coordinates. Notice that preceding each for
statement are the labels xPlane:
and yPlane:
. When the values for each iteration in primeNumbers
are the same, the symmetrical coordinate is logged and the continue xPlane;
statement is used to send the workflow back to the start of the outer loop and avoid making unnecessary iterations over values that aren't symmetrical. The log statement console.log("Processing ", primeNumbers[x], primeNumbers[y]);
confirms the continue xPlane;
statement works, since coordinates where values of the outer loop are larger than the inner loop are never processed.
The second example in listing 6-3 creates a pair of loops to iterate over the vowels
array to place diphthongs in a separate array. Notice that preceding each for
statement are the labels outerLoop:
and innerLoop:
. When the values for each iteration in vowels
are the same, the continue innerLoop;
statement is used which has the effect of a plain continue
statement because it's applied to the inner most loop. If the values for each iteration in vowels
are the same, a diphthong is created with the values and added to the diphthongs
array. Once the tenth diphthong is added to the diphthongs
array, the loop enters the break outerLoop
statement which is used to send the workflow to the outer loop and terminate the loop. Finally, you can see the diphthongs
array is logged with ten diphthong elements in it.
The while
and do while
statements: Loops that run with one expression
An alternative to loops with for
statements, are loops with while
or do while
statements. Unlike for
statements which typically use up to three expressions to define a loop's logic, loops with while
and do while
statements only require one expression to define when a loop ends. This makes while
and do while
statements much simpler to declare, but at the same time it can make them less flexible since any control variable and/or step size adjustment must be made directly in a loop's body.Therefore, loops with while
or do while
statements are best suited when there's no need for a control variable or step size adjustment.
The syntax of the while
and do while
statements consists of an expression to end a loop when it evaluates to false
, as well as a block statement wrapped in curly brackets {}
that contains the logic to execute on every iteration the loop's expression evaluates to true
. The only difference between a for loop with a while
and do while
statement, is the while
statement evaluates its expression before it performs an iteration and the do while
statement evaluates its expression after an iteration is done. This guarantees a do while
for loop runs its block statement at least once and always before a determination is made to finish the loop, while a while
for loop iteration only runs its block statement after it's determined if a loop's work is complete.
Loops with the while
and do while
statements can also use break
and continue
statements to exit loops or iterations prematurely, as well as label statements to alter the workflow of a loop and send it to a predetermined line of code. In addition, it's also possible to create a cleaner endless loop with a while
or do while
statement using the syntax while(true) { }
or do { } while(true)
syntax and relying on a break
statement inside the loop to terminate it.
Listing 6-4 illustrates JavaScript how to create loops with the while
and do while
statements.
Listing 6-4. Loops with while
and do while
statements
let primeNumbers = [2,3,5,7,11]; while (primeNumbers.length) { console.log(primeNumbers.pop()); } let vowels = ["a","e","i","o","u"]; do { console.log(vowels.shift()); } while (vowels.length > 2);
The while
loop in listing 6-4 iterates over the primeNumbers
array. The expression of the while
loop checks primeNumbers.length
, therefore it always does an iteration so long as the primeNumbers
array is not empty. Inside the block statement of the while
loop, the Array
data type's pop()
method is used to remove the last element from primeNumbers
and output said element in a log statement, in this manner, the array eventually empties causing the while
loop expression to be evaluated to false
and finish the loop.
The do while
loop in listing 6-4 iterates over the vowels
array. The expression of the while
loop checks for vowels.length > 2
, therefore an iteration is made so long as the vowels
array has more than elements. Inside the block statement of the do while
loop, the Array
data type's shift()
method is used to remove the first element from vowels
and output said element in a log statement, in this manner, the array eventually reaches a size of 2 causing the do while
loop expression to be evaluated to false
and finish the loop.
The for in
statement: A loop for key-value data structures, the early years
Note The for in
statement is a dated loop syntax. Since its creation, better alternatives have surfaced, including newer loop syntax and data type enhancements to support loops.
The for in
loop syntax was the first construct designed to loop over JavaScript key-value data structures. Unlike the for
, while
and do while
statements which don't necessarily peg loops to data structures, that is, they can run loops with 'n' iterations using counters and conditionals irrespective of a data structure, the for in
statement requires loops to be made against a data structure.
The syntax of the for in
statement consists of a key-value data structure declared after the in
keyword to loop over each element in the data structure. In between the for
and in
keywords is a variable to hold the key of each element on every iteration. After the in
keyword, a block statement in curly brackets {}
is declared to execute business logic on each iteration which has access to the variable declared in between the for
and in
keywords. Like other JavaScript loops, the block statement of a for in
statement can also make use of break
and continue
statements -- including label statements -- to exit a loop or iteration prematurely.
The for in
loop is designed to iterate over key-value data structures using the following rules:
- All key-value elements are processed by a
for in
loop, except those elements that have a key as aSymbol
data type. - All key-value elements are processed by a
for in
loop, so long as an element's value is enumerable.
To fully grasp what these last two rules mean for a for in
loop, it's inevitable to take a closer look at JavaScript data types that have key-value data structures.
The most versatile key-value data structure in JavaScript is the Object
data type and it's precisely this data structure that the for in
loop is more suited to, as well as the data structure where you can potentially face these two rules associated with for in
loops. Now, this doesn't mean the Object
data type is the only key-value data structure in JavaScript, other data types like the Array
data type & the String
data type also operate as key-value data structures, so they too can be run through a for in
loop.
However, because data types like Array
and String
always have elements with simple key integer values and lack the concept of element values being enumerable -- not to mention these data types have other built-in loop mechanisms -- the for in
statement is almost always used on Object
data types.
When it comes to the Object
data type and two for in
rules mentioned in the prior list, you really need to go out of you way to face them. Most keys added to Object
data types aren't Symbol
's and most values added to Object
data types are enumerable by default. To make the key of an Object
data type a Symbol
you need to explicitly create a Symbol
data type and to make the value of an Object
data type non-enumerable you must explicitly set its enumerable property descriptor to false, which can only be done with the Object.defineProperty()
or Object.defineProperties()
methods.
Listing 6-5 illustrates the use of the for in
statement on Object
data types and for the sake of completeness also shows how the for in
statement can be used on String
and Array
data types.
Listing 6-5. For loops with for in
statements
let language = "JavaScript"; let vowels = ["a","e","i","o","u"]; let languages = {JavaScript: { creator:"Eleanor Eich" } } // Loop over String for (const key in language) { console.log(`langauge key: ${key}; value: ${language[key]}`); } // Loop over Array for (const key in vowels) { console.log(`vowels key: ${key}; value: ${vowels[key]}`); } // Loop over Object for (const lang in languages) { console.log(`languages key: ${lang}; value: ${JSON.stringify(languages[lang])}`); } // Loop over nested Object property for (const version in languages.JavaScript) { console.log(`languages.JavaScript key: ${version}; value:${languages.JavaScript[version]}`); } // Add properties to object in various forms // Dot notation languages.TypeScript = {creator:"Microsoft Corporation"}; // Object.defineProperty as not enumerable Object.defineProperty(languages, "Ruby", { value: {creator:"Yukihiro Matsumoto"}, enumerable: false }); // Object.defineProperty as enumerable Object.defineProperty(languages, "Rust", { value: {creator:"Graydon Hoare"}, enumerable: true }); // Bracket notation languages["WebAssembly"] = {creator:"World Wide Web Consortium (W3C)"}; // Bracket notation, with Symbol key languages[Symbol.for("Python")] = {creator:"Guido van Rossum"}; // Inspect languages object // All properties are there and output console.log("languages: %o", languages); // Loop over languages object // Non-enumerable values ("Ruby") and Symbol keys ("TypeScript") are ignored by for in for (const lang in languages) { console.log(`languages key: ${lang}; value: ${JSON.stringify(languages[lang])}`); }
Listing 6-5 starts by declaring three data types: a String
, an Array
and an Object
. Next, you can see for in
loop are applied to each one. In each case, notice the variable declared in between the for
and in
keywords is used inside the loop's block statement, to output a log message with the key and also use the key to access its associated value.
The for in
loop output for String
language
and Array
vowels
confirms the prior statement about keys in these kind of data structures, all keys are integers. In addition, notice the associated values for each key constitute either a String
character or an Array
element. The Object
languages
then has two for in
loop applied to it, the first one loops over the entire language
object, while the second loop uses the JavaScript
key to loop over the nested object languages.JavaScript
.
The more interesting aspect in listing 6-5 is the section where various elements are added to Object
languages
. First, a dot notation is used to add the TypeScript
key with a value of {creator:"Microsoft Corporation"};
. Next, two more elements are added to languages
with the Object.defineProperty()
method: the "Ruby"
key with a property descriptor enumerable: false
and a value of {creator:"Yukihiro Matsumoto"}
; and the "Rust"
key with a property descriptor enumerable: true
and a value of {creator:"Graydon Hoare"}
. Finally, two additional elements are added to languages
with a bracket notation: the "WebAssembly"
key with a value of {creator:"World Wide Web Consortium (W3C)"}
; and the Symbol
key Symbol.for("Python")
with a value of {creator:"Guido van Rossum"}
.
Once the new elements are added to Object
languages
, a plain console
object log statement is used to output the contents in languages
to confirm all new elements are present. However, the last statement in listing 6-5 performs a for in
loop over the updated languages
object where you can see the elements with the "Ruby"
key and Symbol.for("Python")
key aren't output. The reason these elements aren't output, is because the "Ruby"
key value is marked with a property descriptor of enumerable: false
and the key Symbol.for("Python")
is a Symbol
, both of which are skipped in for in
loops.
The Array
data type baked-in for loops, methods with callbacks: every()
, filter()
, forEach()
, map()
, reduce()
& reduceRight()
All the previous for loop statement variations -- for
, while
, do while
and inclusively for in
-- are capable of iterating over an Array
data structure, however, the use of control variables and termination expressions can be tedious to write, specially if all you want to do is iterate over each item in the Array
and be done with it. To that end, the Array
data type supports a series of methods specifically designed to simplify how a loop is executed over each item in the Array
data structure.
The following is a list of iteration methods available in the Array
data type since ECMAScript 5 (ES5):
every()
.- Returns true if each item in the array satisfies a provided testing function.filter()
.- Creates a new array with elements that pass a test function.forEach()
.- Executes a function for each item in the array.map()
.- Creates a new array with the results of each item in the array after passing each item through a function.reduce()
.- Applies a function against an accumulator and each item in the array (from left to right) to reduce it to a single value.reduceRight()
.- Applies a function against an accumulator and each item in the array (from right to left) to reduce it to a single value.some()
.- Retruns true if at least one item in the array satisfies a provided function.
These Array
methods that simplify for loops work with callback functions. This means each of these Array
methods accepts another function as an argument (a.k.a. callback function) that gets run on each of the elements in an Array
. It's just like placing the logic of a block statement in a for loop statement (e.g. for
, while
, do while
) inside a function, to run on each iteration, without worrying about control variables or termination expressions.
Although these Array
methods can produce the same outcome as the for loop statements described earlier, they do operate with the following differences:
- It's not possible to prematurely end a loop as a whole. Once an invocation for one of these
Array
methods starts, all the elements in anArray
must be put through its callback function. - It's possible to prematurely end a single iteration. Like any JavaScript function, a plain
return
statement can be used in a callback function to terminate its workflow, in effect, acting as a for loop's continue statement. - It's possible to access the iteration value, index/counter, as well as the full array inside the callback function. If the callback function has the necessary argument list
element
,index
&array
, all these values can be leveraged within the callback function to perform more elaborate business logic.
Of all the Array
methods that simplify for loops, the one you're most likely to use is the JavaScript forEach()
method. The forEach()
method makes no assumptions about what logic to apply to elements, therefore it's the most generic Array
method to simplify for loops and resembles the for loop statements shown in the previous examples.
Listing 6-6 contains examples of the Array
forEach()
method, including small syntax variations that use plain functions and arrow functions, as well as a return
statement to prematurely exit iterations.
Listing 6-6. Array forEach()
method
let primeNumbers = [2,3,5,7,11]; // Array forEach() primeNumbers.forEach(function (element, index, array) { console.log(element); console.log("index:", index); console.log("array:", array); }); // Equivalent with ES6 arrow function primeNumbers.forEach((element, index, array) => { console.log(element); console.log("index:", index); console.log("array:", array); }); // Array forEach() with return primeNumbers.forEach(function (element) { // Move to next iteration when element > 4 & < 10 if (element > 4 && element < 10) { return; } console.log(element); }); // Equivalent with ES6 arrow function primeNumbers.forEach(element => { if (element > 4 && element < 10) { return; } console.log(element); })
The first forEach()
example in listing 6-6 loops over all the elements in the primeNumbers
array, just like it's done in the examples in listing 6-1 with a for
statement. Notice the callback function uses the arguments element
, index
& array
to access each of these value on every iteration and output them with a log statement. The second forEach()
example uses an arrow function to simplify the callback function syntax and produce the same outcome. Because arrow functions were introduced in ECMAScript 6 (ES2015) -- after the Array
forEach()
method -- there's a small time gap where the forEach()
method only worked with inline callback functions, so both alternatives are valid, albeit arrow functions are the modern approach.
The third and fourth forEach()
examples in listing 6-6 make use of a return
statement, so in case a primeNumbers
value is between 4
and 10
, the function immediately concludes and the next iteration starts. This behavior is like the example in listing 6-2 with a for
and continue
statement. In addition, notice these last callbacks only use the single argument element
vs. three arguments like the first two examples in listing 6-6. The only difference between the third and fourth examples, is the third example uses an inline callback function, while the fourth example uses an equivalent arrow callback function.
As outlined in the iteration method list, the other Array
methods that simplify for loops are designed to fulfill more specific tasks. Listing 6-7 illustrates examples for each of the methods.
Listing 6-7. Array other for loop methods: every()
, filter()
, map()
, reduce()
, reduceRight()
, some()
let primeNumbers = [2,3,5,7,11]; let pageHits = ["index.html", "syntax.html", "index.html", "for-loops.html", "index.html","asynchronous.html","syntax.html"]; let primeNumbers = [2,3,5,7,11]; let pageHits = ["index.html", "syntax.html", "index.html", "for-loops.html", "index.html","asynchronous.html","syntax.html"]; // Start suport functions for iteration methods function isPrime(element, index, array) { return element / 1; } function evenNumber(element, index, array) { if (element % 2 === 0) return element; } // Custom function for ES5 // can be replaced with ES6 arrow function: element => element.toUpperCase() function allCaps(element, index, array) { return element.toUpperCase() } function countPageHits(allPages, page) { if (page in allPages) { allPages[page]++; } else { allPages[page] = 1; } return allPages; } // End support functions for iteration methods // Array every() console.log(primeNumbers.every(isPrime));//true console.log(primeNumbers.every(evenNumber));//false // Array filter() console.log(primeNumbers.filter(evenNumber));//[2] // Array map() // ES5 with custom function console.log(pageHits.map(allCaps)); // ES6 with array function console.log(pageHits.map(element => element.toUpperCase())); // Array reduce() console.log(pageHits.reduce(countPageHits,{})); // Array reduceRight() console.log(pageHits.reduceRight(countPageHits,{})); // Array some() console.log(primeNumbers.some(evenNumber));//true
The first half of listing 6-7 declares a series of support functions to use as callback functions on each of the Array
functions that simplify for loops. These support functions include: isPrime()
which determines if a number is prime; evenNumber()
which determines in a number is even; allCaps()
which returns the value of a string in upper case letters; as well as countPageHits()
which counts the number of occurrences of an element in a given array.
Next, you can see in listing 6-7 how the Array
every()
method is used to evaluate if all the elements in the primeNumbers
array are prime and odd, leveraging the isPrime()
and evenNumber()
callback functions, respectively. You can see primeNumbers.every(isPrime)
returns true
and primeNumbers.every(evenNumber)
returns false
. Then the Array
filter()
method is used with the evenNumber()
callback function to get all the even numbers in the primeNumbers
array, in this case, you can see the result is [2]
.
Next, you can see in listing 6-7 two calls made on the pageHits
array with the Array
map()
method to apply a function to all the elements in an array. The two calls made with the map()
method use different syntax and callback functions. The first map()
call uses the support function allCaps()
also in listing 6-7 to turn every element in the pageHits
array to upper case, while the second map()
call uses an arrow function to directly invoke the String
data type upperCase()
method to convert every element in the pageHits
array to upper case.
The purpose of the Array
reduce()
and reduceRight()
method calls in listing 6-7 is to apply a reducer function[2] -- in this case countPageHits()
-- to obtain the sum of all the elements in an array, in this case, the pageHits
array. The only difference between reduce()
and reduceRight
is the order in which the reducer function is applied to the array, with reduce()
applying the reducer function from left (start of an array) to right (end of an array) and reduceRight()
applying it from right (end of an array) to left (start of an array). This is the reason applying reduce()
to the pageHits
array results in the keys being ordered from first to last appearance in the array (e.g. {'index.html': 3, 'syntax.html': 2, 'for-loops.html': 1, 'asynchronous.html': 1}
) and applying reduceRight()
to the same array results in the keys being order from last to first appearance (e.g. {'syntax.html': 2, 'asynchronous.html': 1,'index.html': 3,'for-loops.html': 1}
), with the results being identical for both methods if order is ignored.
Finally, the last statement in listing 6-7 illustrates how the Array
some()
method checks if at least one element in the primeNumbers
array is an even value, leveraging the evenNumber()
callback function. You can see primeNumbers.some(evenNumber)
returns true
, since 2
is an even number.
The Map
data type & Set
data type baked-in for loop forEach()
method with callback
Following in the steps of the Array
forEach()
method, the Map
data type and Set
data type also have a forEach()
method, intended to simplify how a loop is executed over all the elements in a Map
or Set
data structure by means of a callback function.
The Map
forEach()
method and Set
forEach()
method -- including their callback function -- have the same behaviors and limitations as the Array
forEach()
method described in the previous section: It's not possible to prematurely end a loop as a whole, once an invocation starts, all elements must be put through the callback function; it's possible to prematurely end a single iteration with a plain return
statement in a callback function to terminate its workflow; and it's also possible to access the iteration value, index/counter, as well as the full data structure inside the callback function.
Listing 6-8 contains examples of the Map
forEach()
and Set
forEach()
methods, including small syntax variations that use plain functions and arrow functions, as well as a return
statement to prematurely exit iterations.
Listing 6-8. Map forEach()
method & Set forEach()
method
let jsFramework = new Map([["name","React"],["creator","Facebook"],["purpose","UIs"]]); let vowels = ["a","e","i","o","u","e","i","o","a","u","u"]; let vowelsSet = new Set(vowels); // Map with forEach() jsFramework.forEach(function (value, key, map) { console.log(value); console.log("key:", key); console.log("map:", map); }); // Set with forEach and ES6 arrow function vowelsSet.forEach((value, index, set) => { console.log(value); console.log("index:", index); console.log("set:", set); }); // Map forEach() with return jsFramework.forEach(function (value) { // Move to next iteration when value "React" if (value == "React") { return; } console.log(value); }); // Equivalent with ES6 arrow function vowelsSet.forEach(value => { // Move to next iteration when value "e" if (value == "e") { return; } console.log(value); });
Listing 6-8 starts by declaring the jsFramework
map, followed by the vowels
array that's used as the source to define a unique set of values in the vowelsSet
set. Next, you can see the first forEach()
example loops over all the elements in the jsFramework
map, be aware the argument order for the callback function is value, key, map
, which can be confusing for key-value data types like Map
where you might expect the key to be first followed by the value, neverthless, this argument order adheres to the same conventions used in other data type forEach()
methods. The second forEach()
example loops over all the elements in the vowelsSet
set, in this case, you can see the callback function uses arrow function syntax and outputs its three arguments: the value
for a given iteration in the set, the index
value for a given iteration and the set
with all the values of the data structure.
The final two forEach()
examples loop over the jsFramework
map and vowelsSet
set, with both making use of a return
statement to prematurely exit certain iterations. Each of these examples uses different callback syntax, with the jsFramework
map using an inline callback function and the vowelsSet
an arrow callback function. In addition, notice the callbacks for these last two forEach()
examples only use a single argument to access an iteration's value, confirming what was mentioned in the earlier Array
forEach()
method, callback functions used in forEach()
methods can optionally declare up to three arguments to access an iteration's element value , its index/counter, as well as the full data structure.
If you're wondering why the Map
and Set
data types are limited to the forEach()
method to perform loops, while the Array
data type supports the forEach()
method, as well as the every()
, filter()
, map()
, reduce()
, reduceRight()
& some()
methods to simulate more specialized loop logic, the reason is due to ECMAScript versioning and feature support. All these Array
methods that use callback functions were introduced in ECMASCript 5 (ES5), while the Map
and Set
data types were introduced in ECMAScript 6 (ES2015). However, as part of ECMAScript 6 (ES2015) an enhanced for loop mechanism was added in the form of iterables and iterators, which is discussed in the upcoming sections. So the lack of methods to simulate for loops with callback functions in Map
and Set
data types, is because they rely more on the concept of iterables and iterators to perform loops.
The for of
statement: Iterable and iterator protocols
The iterable and iterator protocols[3] are among the most important additions to ECMAScript 6 (ES2015) and they're directly tied to for loops and the for of
statement. The term protocol stems from the fact iterable and iterator are conventions followed by certain JavaScript data types. Like all protocols, they can be either followed or ignored, so a JavaScript data type may or may not support the iterable and iterator protocols. In ECMAScript 6 (ES2015), almost all JavaScript data types are -- or can become -- iterables.
So what is an iterable ? It's an object that has elements you can loop over. An iterable 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).
An iterable represents a more powerful approach to loop over data structures, because iterables work in conjuction with iterators. Iterators represent the mechanism to move over iterables. In most cases, an iterator walks through all the elements in an iterable, but it's possible to customize or terminate an iterator prematurely just like it's done in regular loops (e.g. using the break
or continue
statement).
In order to work with iterables and iterators you typically use the for of
statement, which is specifically designed to loop over iterables. The general syntax to create a loop over an iterable with the for of
statement is the following:
for (let <reference> of <iterable_object>) { console.log(<reference>); }
As you can see, for loops with the for of
statement have a pretty straightforward syntax. But before you completely forget about the earlier for loop syntax techniques and drop them in favor of for of
statements, it's very important to understand that for of
statements only work on iterable objects.
Listing 6-9 illustrates how to create for loops with the for of
statement.
Listing 6-9. 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}`); } // 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); }
The first three examples in listing 6-9 perform a loop over a String
, an Array
and a Map
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. It's worth pointing out a Map
data type is used instead of an Object
object data type, because an Object
object data type isn't an iterable, so it can't be used directly in a for of
statement -- but more details on this in the next section. The last two examples in listing 6-9 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.
Iterable objects and the next()
method: Behind the scenes of the for of
statement & manually advancing over iterables
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-9 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 -- although 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 implicitly calls an iterable object's next()
method and it's this 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'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 is done.
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 next()
method on them, or more practically, candidates to run through a for of
statement.
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-9 which simply use standard data types that support the @@iterator
method.
Tip TheObject
object data type due to its ample and legacy baggage can't be converted to an iterable object. However, throughout the years this data type has added methods that allow it to be converted to anArray
data type (e.g.Object.entries()
,Object.keys()
,Object.values()
). This in turn allows anObject
data type to be converted to anArray
first which can then be converted to an iterable object withArray
methods that produce iterable objects.
Another alternative is to use aMap
data type which does support being converted to an iterable object vs. anObject
data type.
Array
data type methods that produce iterable objects: entries()
, keys()
, values()
& [Symbol.iterator]()
Iteration methods available in the Array
data type are the following:
entries()
.- Creates a new Array with key/value pairs where each key is the index of the array. Helpful as it provides built-in counter in the form of the key for each array value.keys()
.- Creates a new Array with the keys for each index in the array. Helpful as a counter array.values()
.- Creates a new Array with the values for each index in the array.Array[Symbol.iterator]()
.- Works likevalues()
by default.
Generators and yield expressions
A problem with loops made against large data structures -- hundreds or thousands of items -- is efficiently processing them in memory. If you consider looping over large data structures is often associated with sequential operations (e.g. 1,2,3,4,5,6,7...) an Array
or Object
data type per se -- even as an iterable -- they represent a weak choice for large data structures. The introduction of the for of
statement, iterable and iterator protocols, gave way to generators and yield expression which are designed to tackle this issue.
Generators are iterators embodied as functions. In a standard function you call the function and it runs uninterrupted until it finds return
. In a generator function -- which are simply called generators -- you can integrate pauses into a function so that each time it's called, it's a continuation of the previous call. In addition, generators have the ability to generate values on-demand -- which is why they get their name -- so they're much more efficient when it comes to handling large data ranges.
To create a generator you append the *
to a function
declaration, as illustrated in listing 6-11
Listing 6-11. Generators with the *
syntax
function* myGenerator() { yield 2; yield 3; yield 5; } let g = myGenerator(); console.log(g.next().value) // outputs 2 console.log(g.next().value) // outputs 3 console.log(g.next().value) // outputs 5
The myGenerator()
generator is unconventional and is intended to illustrate the basic use of yield expressions with the yield
keyword. The yield
keyword works as a combination of return & stop behavior. Notice the generator is assigned to the g
reference, once this is done, you can start stepping through the generator with the iterable next()
method -- after all, a generator is an iterator.
On the first next()
call, the generator gets to yield 2
, where it returns 2
and stops until next()
is called again. On the second next()
call the generator gets to yield 3
, it returns 3
and stops until next()
is called again. This process goes on until the last yield
is reached and the next()
method return { value: undefined, done: true }
just like all iterators do.
With this initial overview of generators and the purpose of the yield
keyword, let's analyze a few real-life scenarios that can benefit from these techniques.
- A large data array that would otherwise take up a lot of memory to entirely load in one step.
- Launching tasks to run in parallel with the main program flow.
- Running a task at discretionary times without blocking the main program flow (i.e. asynchronosuly).
Listing 6-12 illustrates a more realistic example of JavaScript generators, where the generator is designed to return a sequence of prime numbers with the potential to return an infinite amount of prime numbers with minimal memory consumption (i.e. the prime numbers don't need to be hard-coded, they're generated on-demand).
Listing 6-12. Generators
// Generator to calculate prime numbers function* primeNumbers() { let n = 2; while (true) { if (isPrime(n)) yield n; n++; } function isPrime(num) { for (let i = 2; i <= Math.sqrt(num); i++) { if (num % i === 0) { return false; } } return true; } } // Create generator let primeGenerator = primeNumbers(); // Advance through generator with next(), print out value console.log(primeGenerator.next().value); console.log(primeGenerator.next().value); console.log(primeGenerator.next().value); console.log(primeGenerator.next().value); console.log(primeGenerator.next().value); // Calls to primerGenerator.next() returns infinite prime numbers
Notice how the primerNumbers
generator function in listing 6-12 doesn't declare a hard-coded prime numbers array like the previous examples. Internally, the primerNumbers
generator starts with a value of n=2
and increases this value through an endless loop (i.e.while(true)
) yielding a result each time n
evaluates to a prime number via the logic in isPrime
.
Next, you can see the primerNumbers
generator is initialized and multiple calls are made to the next()
method to advance through the generator/iterator. Finally, value
is extracted from each next()
method result to output the given prime number in the iteration.
By using this technique, you effectively generate prime numbers as they're needed, instead of declaring and loading them in a single step. This is particularly helpful and more efficient for cases where there's a potential for hundreds or thousands of elements.