Loops and iteration
Introduction
In computing a loop specifies iteration. With iteration a statement is executed repeatedly a specified number of times,
once for each item in a collection or until some condition is met.
A modern high-level programming language generally
supports count-controlled loops (for
loops),
condition-controlled loops (while
loops and do...while
loops) and
collection-controlled loops (for each
loops).
Recursion is closely related to iteration. Recursion is implemented by a function calling itself. Recursion will be discussed in a later chapter. Iteration and recursion are key elements of algorithms.
for
statement
The JavaScript syntax of the count-controlled for
loop is:
for (initialization; condition; increment) statement;
After the for
keyword three expressions enclosed in parentheses follow.
They are separated by semicolons. The expressions are optional, but the semicolons are not.
There must be precisely two semicolons within the parentheses. The three optional expressions are:
initialization
:- This expression is evaluated once, before the loop begins. Typically this expression declares and initializes one or more loop counter variables.
condition
:-
This expression is evaluated before each loop iteration.
If the expression is truthy, the
statement
is executed, if the expression is falsy, the loop terminates. If this expression is omitted, it is assumed to be truthy and the loop will never stop (an infinite loop), unless thestatement
contains code to break out of the loop. increment
:-
This expression is evaluated at the end of each loop iteration.
That is, after executing the
statement
and before the next evaluation ofcondition
. Typically this expression updates or increments the counter variable(s).
The statement
(usually a block statement) is the statement to be repeated.
Next an example of how a simple for
-loop typically is arranged.
The statement
successively logs the digits 0 to 9. Note that you can also use
++i
instead of i++
.
for (let i = 0; i <= 9; i++) {
console.log(i);
}
The next example does the same, but now the initialization is done outside the loop and the condition and increment expressions are placed inside the repeating block statement.
let i = 0;
for (;;) {
if (i <= 9) {
console.log(i);
i++;
} else break; // do not forget to include this break!!
}
Warning: the condition expression is now moved from inside the parentheses to the statement.
This means that you need to use a break
statement to explicitly break out the loop if the condition becomes falsy.
Otherwise you have created an infinite loop which will stall your application and computer when running the code.
As an exercise: what will be logged to the console in the next two scripts?
for (let i = 0, n = 0; i < 5; ++i){
console.log(n += i);
}
for (let i = 0, j = 0; i < 5; ++i){
for (; j < 5; ++j){
console.log(i+", "+j);
}
}
for...in
statement
The JavaScript collection-controlled for...in
statement iterates over the properties of an object.
const myObject = { a: 1, b: 2, c: 3 };
for (let propertyName in myObject) {
console.log(propertyName + ": " + myObject[propertyName]);
}
// logs:
// "a: 1"
// "b: 2"
// "c: 3"
A different property name (a string) is assigned to propertyName
on each iteration.
The loop terminates once the statement has been repeated for every property.
Three things in particular are important to keep in mind:
-
A
for...in
loop also iterates over inherited properties (inherited from its prototype chain). This may not be desired for your application. You can useObject.hasOwn()
to iterate over the object's own properties only; the inherited properties will not be displayed. -
A
for...in
loop iterates over the properties of an object in an arbitrary order. The order is browser/platform depended. This means that modifying properties during iteration should only be done on the property currently being visited and it should be a general modification, not intended for a specific property. -
A
for...in
loop only iterates over enumerable properties of an object. What "enumerable properties" are will be explained later in Enumerability of properties. By default, properties are enumerable.
Use
Object.hasOwn()
to iterate over the object's own properties only:
const myObject = { foo: true, bar: true };
for (let propertyName in myObject) {
if (Object.hasOwn(myObject, propertyName)) {
console.log(propertyName + ": " + myObject[propertyName]);
}
}
BTW: This could also have been achieved by using hasOwnProperty()
, but Object.hasOwn()
is less prone to problems and therefore recommended.
It is also possible to create an array with all object's own properties by using
Object.keys()
or
Object.getOwnPropertyNames()
.
Technically a for...in
loop can also be used to iterate over array elements or over strings.
But a for
loop, for...of
loop or Array.prototype.forEach()
method are better options for these purposes
(see next sections).
for...of
statement
The JavaScript collection-controlled for...of
statement iterates over
iterable objects in a successive ascending order.
Iterable objects are objects with a special "iterator" method, such as Array
, Map
and Set
(later this tutorial more about this).
Unlike the for...in
statement,
the for...of
statement does not work on objects in general, only on iterable objects.
Iterating over an Array
const myArray = [10, 20, 30];
for (let elementValue of myArray) {
console.log(elementValue);
}
// logs:
// 10
// 20
// 30
The elements of an array are objects with properties with successive integer names (indexes), from zero to the end of the list.
In that order, a next property value is assigned to elementValue
on each iteration.
Unlike with the for...in
statement,
with a for...of
loop the values, instead of the property names (indexes), are assigned to elementValue
.
The loop terminates once the statement has been repeated for every successive property.
Iterating over a String
As mentioned above, for...of
loops iterate over iterables.
Examples of iterables are Array
, Map
and Set
.
But also a string (via wrapper object String
) is an iterable. In next example a for...of
loop iterates over a string.
const string = 'LOL!';
for (let character of string) {
console.log(character);
}
// Logs:
// "L"
// "O"
// "L"
// "!"
The for...of
loop works correctly on strings with surrogate pairs.
const string = 'ππ€©'; // surrogate pairs
for (let character of string) {
console.log(character);
}
// Logs:
// "π"
// "π€©"
However, strings with assembled characters will break. More sophisticated methods are needed to distinguish the separate assembled characters in a string (see regular expressions).
const string = 'π§πΏπ¨πΏββ'; // assembled characters
for (let character of string) {
console.log(character);
}
// Logs:
// "π§"
// "πΏ"
// "π¨"
// "πΏ"
Alternatives for the for...of
loop
A common for
loop can often be used as an alternative for the for...of
loop, as shown in the next example.
In the conditions we use the length
property, respectively inherited from the Array
prototype chain and
via the wrapper object String
.
const myArray = [10, 20, 30];
for (let i = 0; i < myArray.length; i++) {
console.log(myArray[i]);
}
const string = 'LOL!';
for (let i = 0; i < string.length; i++) {
console.log(string.charAt(i));
}
However, not every iterable has a length
property, so a common for
loop cannot be used on all iterables.
And a common for
loop is not Unicode-aware when looping over a string.
const string = 'ππ€©'; // surrogate pairs
for (let i = 0; i < string.length; i++) {
console.log(string.charAt(i));
}
// Logs:
// "οΏ½"
// "οΏ½"
// "οΏ½"
// "οΏ½"
Of course, we could get around this by using a Unicode-aware method, such as Array.from()
, to create an additional array with each character as one element.
However, that would be unnecessarily complicated since we have the Unicode-aware for...of
loop.
Array
also inherits a forEach()
method which can be used to iterate over an array.
const myArray = [10, 20, 30];
myArray.forEach(function(element) {
console.log(element)
})
// Logs:
// 10
// 20
// 30
Technically you can also use the
Array.prototype.map()
method.
This method creates a new array populated with the results of calling a provided function on every element in the calling array.
But using this method is an anti-pattern if the returned array
is not used (as in this case) and/or if the callback does not return a value (as in this case).
const myArray = [10, 20, 30];
// Do not do this:
myArray.map(function(element) {
console.log(element)
})
// Logs:
// 10
// 20
// 30
while
statement
The JavaScript syntax of the condition-controlled while
loop is:
while (condition) statement;
condition
:-
This expression is evaluated before each loop iteration.
If the expression is truthy, the
statement
is executed, if the expression is falsy, the loop terminates. statement
:- This is the statement (usually a block statement) to be repeated.
Next an example:
let n = 0;
while (n < 3) {
console.log(n);
n++;
}
// logs:
// 0
// 1
// 2
What would have been logged to the console if n++;
was before console.log(n);
?
Note that any while
loop can also be constructed with a for
loop and that also with a
while
loop you can create an infinite loop:
// do not run this code!
while(true);
do...while
statement
The JavaScript syntax of the condition-controlled do...while
loop is:
do statement while (condition);
statement
:- This is the statement (usually a block statement) to be repeated. The statement is executed at least once.
condition
:-
This expression is evaluated after each loop iteration.
If the expression is truthy, the
statement
is re-executed, if the expression is falsy, the loop terminates.
Next an example:
let n = 0;
do {
console.log(n);
n++;
}
while (n < 3);
// logs:
// 0
// 1
// 2
What would have been logged to the console if n++;
was before console.log(n);
?
Note that the statement is always executed at least once, even if the condition is false under all circumstances. Also note that with a
do...while
loop you can also create an infinite loop.
const condition = false;
do console.log("This will be logged, even though the condition is false")
while (condition);
continue
, break
and label
continue
and break
let txt = '';
for (let i = 0; i < 5; i++) {
if (i === 3) {
continue;
}
txt = txt + i;
}
console.log(txt); // logs: "0124"
In the example above the for
loop iterates over the statement,
but breaks out of the iteration where i === 3
.
The continue
statement breaks out of one (the current) iteration,
the break
statement breaks out of the entire loop.
The continue
statement can be used inside the body (the repeated statement) of a
loop. The break
statement can be used inside the body of a loop or
switch
statement.
In a for
loop the increment expression is evaluated at the end of each loop iteration.
When a continue
statement breaks out of an iteration, the increment expression will still be evaluated.
Using continue
with a label
The continue
statement can be used with a label
reference.
A label
adds an identifier to a loop, like myLabel:for(;;);
. You can refer to that identifier
in a continue
statement, like continue myLabel
.
The label
statement is used in nested loops. Next example shows how this works.
myLabel:
for (let i = 0; i < 3; ++i) { // labeled loop
for (let j = 0; j < 3; ++j) { // nested loop
if (i === 1 && j === 1) {
continue myLabel;
}
console.log(i+", "+j);
}
}
// logs:
// "0, 0"
// "0, 1"
// "0, 2"
// "1, 0"
// "2, 0"
// "2, 1"
// "2, 2"
In the example above the continue
statement breaks out of the current iteration,
but not of the nested loop where the continue
statement sits. It breaks out of the current iteration of the labeled (the parent) loop.
In other words; it jumps to the next iteration of the outer (labeled) loop instead of to the next iteration of the inner (nested) loop.
If the continue
statement would not have had a label reference,
it would only have skipped one iteration in the inner loop (only "1, 1"
)
and not two iterations in the inner loop (not both "1, 1"
and "1, 2"
).
Using break
with a label
Previously we already used the break
statement in switch
statements and in loops without a condition expression.
A break
statement breaks out of the directly enclosing for
, while
or do...while
loop.
A break
statement can also be used with a label
reference in a nested loop, much like how a continue
statement
with a label
works.
myLabel:
for (let i = 0; i < 3; ++i) { // labeled loop
for (let j = 0; j < 3; ++j) { // nested loop
if (i === 1 && j === 1) {
break myLabel;
}
console.log(i+", "+j);
}
}
// logs:
// "0, 0"
// "0, 1"
// "0, 2"
// "1, 0"
In the example above break myLabel
terminates the outer (labeled) loop when i === 1 && j === 1
.
It terminates the entire outer loop where the continue myLabel
statement earlier only terminated the current iteration of the outer loop.
If the break
statement had no label reference, it would only have terminated the inner loop.
Note that in this example, using either break
(without a label) or continue myLabel
will have the same effect:
myLabel:
for (let i = 0; i < 3; ++i) {
for (let j = 0; j < 3; ++j) {
if (i === 1 && j === 1) {
break; // or: continue myLabel
}
console.log(i+", "+j);
}
}
// logs:
// "0, 0"
// "0, 1"
// "0, 2"
// "1, 0"
// "2, 0"
// "2, 1"
// "2, 2"