What a nested loop is
A nested loop is a loop placed inside the body of another loop. The outer loop starts a pass, the inner loop then runs from its first iteration to its last, and only after that does the outer loop move to its next pass. Every pass of the outer loop restarts the inner loop from scratch.
You reach for nested loops whenever the data or the output has two dimensions: rows and columns of a grid, an array of arrays, or every pair of items in a list. Nested for loops are the most common form, but any loop can sit inside any other, including while inside for and for...of inside for.
A nested for loop, step by step
Here is the shape of the lesson's exercise, a rectangle of stars with 5 rows and 10 columns. The outer loop counts rows and the inner loop counts columns:
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 10; col++) {
process.stdout.write("*");
}
process.stdout.write("\n");
}
**********
**********
**********
**********
**********
To build it:
- Write the outer loop with a variable
rowthat starts at 0 and runs whilerow < 5. It makes one pass for each row. Its body does not readrow, which is fine: the variable only counts the passes. - Write the inner loop inside it with
colrunning from 0 whilecol < 10. It makes one pass for each column of the current row. - Print one star per inner pass with
process.stdout.write("*"). Unlikeconsole.log, which always ends its output with a newline,process.stdout.writeprints exactly the text it is given, so the 10 stars of a row stay on one line. - Print the newline after the inner loop, still inside the outer loop. Put it inside the inner loop and every star lands on its own line; put it after the outer loop and there is a single line break at the very end.
The inner body runs once per pass of the outer loop times once per pass of the inner loop, so here the star line runs 5 × 10 = 50 times. This multiplication is also how you estimate the cost of nested loops: two loops that each run 100 times execute the innermost line 10,000 times.
process.stdout.write exists in Node.js. In a browser console, build each row as a string and print it once with console.log:
for (let row = 1; row <= 4; row++) {
let line = "";
for (let col = 0; col < row; col++) line += "*";
console.log(line);
}
*
**
***
****
This one also shows that the inner loop's limit can depend on the outer variable. The inner loop runs row times, so each row is longer than the one before.
Looping over a two-dimensional array
A grid in JavaScript is an array whose items are arrays. The outer loop picks a row; the inner loop walks that row. The inner limit is arr[i].length, the length of the current row, not arr.length:
const arr = [
[1, 2], [3, 4], [5, 6]
];
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr[i].length; j++) {
console.log(arr[i][j]);
}
}
This prints 1, 2, 3, 4, 5 and 6, one per line. When you only need the values and not the positions, for...of is shorter and removes the chance of an off-by-one error:
for (const row of arr) {
for (const value of row) {
console.log(value);
}
}
The same nesting works with forEach callbacks, but a break or continue is not allowed inside one, because the body is a function and not a loop. return in a callback only leaves that one call. Use for or for...of when you need to stop early.
Using both loop variables
The two variables together give you a position, which is how a multiplication table is built:
for (let i = 1; i <= 3; i++) {
let line = "";
for (let j = 1; j <= 3; j++) line += (i * j) + " ";
console.log(line.trim());
}
1 2 3
2 4 6
3 6 9
Starting the inner loop at i + 1 instead of 0 visits every pair of items once, without pairing an item with itself or repeating a pair in the other order. With nums = [1, 2, 3] and j starting at i + 1, the pairs are 1 2, 1 3 and 2 3.
Breaking out of nested loops
A break stops only the loop it is written in. Placed in the inner loop, it ends the inner loop and the outer loop carries on with its next pass:
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr[i].length; j++) {
if (arr[i][j] == 3) {
break;
}
console.log(arr[i][j]);
}
}
This prints 1, 2, 5 and 6. When the inner loop reaches 3 (at i = 1, j = 0) it stops, so 4 is skipped, and the outer loop goes on to i = 2 and prints the whole last row.
A break in the outer loop, outside the inner one, ends everything. This version checks the first value of each row before the inner loop starts:
for (let i = 0; i < arr.length; i++) {
if (arr[i][0] == 3) {
break;
}
for (let j = 0; j < arr[i].length; j++) {
console.log(arr[i][j]);
}
}
It prints 1 and 2 only, because the second row starts with 3 and the outer loop ends there.
Leaving both loops at once
To stop all the looping from inside the inner loop, give the outer loop a label and break to it:
outer: for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr[i].length; j++) {
if (arr[i][j] === 3) break outer;
console.log(arr[i][j]);
}
}
This prints 1 and 2. Inside a function you can also use return, which leaves every loop at once and is often the clearer choice. continue follows the same rule as break: it skips to the next pass of the loop it is in, so in the inner loop it skips the rest of one inner pass.
Common mistakes
- Reusing the same variable name. If the inner loop declares its own
let ithe code still runs, because the innerihides the outer one. But the outer value is out of reach inside the inner loop, which is nearly always a bug. Use different names such asrowandcol, oriandj. - Using
varin the loops. Avarvariable belongs to the whole function, so an inner loop that reuses the name of an outervarchanges the outer counter and can make the outer loop misbehave. Declare loop counters withlet. - Using the wrong length for the inner bound. For rows of different lengths,
arr.lengthin the inner loop reads past the end of short rows and givesundefined. Usearr[i].length. - Putting the newline in the wrong place. A newline inside the inner loop breaks every row into single characters. After the outer loop it joins all rows into one.
- Expecting
breakto leave both loops. It leaves only the innermost one. Use a label orreturnto leave both. - Ignoring the cost. Nested loops over the same input of size
nrun aboutn * ntimes. That is fine for small inputs and slow for large ones, so look for a simpler approach when the input can be big.
Key takeaways
- The inner loop runs completely for every pass of the outer loop.
- The total work is the outer count times the inner count.
- For a 2D array, loop over rows first, and bound the inner loop by the current row's length.
breakandcontinueaffect only the loop that contains them. Use a label orreturnto leave both loops.- Give the loops different variable names and declare them with
let.
