JAVASCRIPT / LOOPS
for loops and counting with an index
Write for loops whose header drives a counter, use that index to read and place array elements, and stop exactly one step before length.
What you will learn
- Read a for header as: init once, test before each pass, update after each pass
- Use i < arr.length so the final read is arr[arr.length - 1], not undefined
- Count down or skip elements by changing only the start, the test, and the update
- Explain why let gives each pass its own i while var shares a single binding
Understanding for loops and counting with an index
A for loop header holds three separate expressions separated by semicolons, and each one runs at a different moment. The initialiser runs exactly once before anything else, the condition is evaluated immediately before every pass through the body, and the update runs after the body finishes a pass, just before the condition is tested again. Once you hold that order in your head, the loop stops being a magic incantation: it is a small state machine around one number, and the array is only what you happen to look at with that number.
That number is a position, not a value, and JavaScript array positions start at 0. An array of length 4 has valid indices 0, 1, 2 and 3, so i < arr.length is the test that produces exactly length passes and ends with the last real element. Reading past the end is not an error in JavaScript — arr[4] simply evaluates to undefined — which is why an off-by-one loop often looks fine until that undefined reaches a method call or a string and blows up somewhere else. Zero-based indices also make position arithmetic clean: with a fixed row width, Math.floor(i / width) is the row and i % width is the column, with no correction terms.
Controlling the index directly is the whole reason to reach for a counting for loop instead of iterating values. You can step by two, walk backwards, compare arr[i] with its neighbour arr[i + 1], or print a human-friendly 1-based position as i + 1 while still indexing from 0. Declaring the counter with let matters here too: let creates a fresh binding for each iteration, so a function created inside the body captures the value that pass had, while var creates one binding shared by every pass, which is why closures made with var all report the final number.
const colors = ["red", "green", "blue", "amber"];
for (let i = 0; i < colors.length; i++) {
console.log(i + ": " + colors[i]);
}
console.log("highest valid index: " + (colors.length - 1));
for (let i = colors.length - 1; i >= 0; i--) {
console.log("backwards " + i + " -> " + colors[i]);
}A for loop's header packages a counter's start, stop test, and step in one place, so what you are really controlling is an index, not an element.
Worked examples
Stepping two indices at a time
A step of 2 lets one loop read pairs of neighbouring slots from a flat array.
const flat = ["a", 1, "b", 2, "c", 3];
for (let i = 0; i < flat.length; i += 2) {
console.log(flat[i] + "=" + flat[i + 1]);
}Example explained
Line 1i += 2 as the update makes each pass advance two positions instead of one, so i takes the values 0, 2 and 4.
Line 2flat[i + 1] reads the partner slot inside the same pass, so no second loop is needed to pair items up.
Line 3The loop ends when i becomes 6, because 6 < 6 is false; had the array length been odd, flat[i + 1] on the last pass would have been undefined.
let and var capture different counters
Functions created inside the body show that let rebinds the counter per pass while var does not.
const withLet = [];
for (let i = 0; i < 3; i++) {
withLet.push(() => i);
}
const withVar = [];
for (var j = 0; j < 3; j++) {
withVar.push(() => j);
}
console.log(withLet[0](), withLet[1](), withLet[2]());
console.log(withVar[0](), withVar[1](), withVar[2]());
console.log(j);Example explained
Line 1let i creates a new binding for every pass, so each arrow function closes over its own copy and returns 0, 1 and 2.
Line 2var j creates a single binding for the whole enclosing scope, so all three arrows read the same variable, which holds 3 once the loop has exited.
Line 3console.log(j) works because var is not block-scoped; the same line written with the let counter would throw a ReferenceError since i does not exist outside the loop.
Turning one index into row and column
Index arithmetic maps a flat position onto a grid without a second loop.
const cells = ["a", "b", "c", "d", "e", "f"];
const width = 3;
for (let i = 0; i < cells.length; i++) {
const row = Math.floor(i / width);
const col = i % width;
console.log(`i=${i} row=${row} col=${col} value=${cells[i]}`);
}Example explained
Line 1Math.floor(i / width) counts how many complete rows of 3 fit below position i, which is the row number.
Line 2i % width is the remainder after those complete rows, which is the offset inside the current row.
Line 3Both formulas work without adding or subtracting 1 precisely because indices and rows both start at 0.
Important notes
The condition is tested before the first pass, so a loop over an empty array runs its body zero times and needs no length check in front of it.
i++ and ++i behave identically in the update clause because the resulting value is discarded; and with let the counter ceases to exist after the loop, so declare it outside the header if you need its final value.
Common mistakes
Writing i <= arr.length: the loop runs one extra pass and reads arr[arr.length], which is undefined, so you either print a stray "undefined" or crash with a TypeError when you call a method on it.
Incrementing i inside the body as well as in the update clause: the counter advances by two per pass and every second element is silently skipped with no error to point at it.
Calling arr.splice(i, 1) while looping upwards: the remaining elements shift down one index while i moves up, so the item directly after each removal is never examined.
Try it yourself
Change, predict, then run
In a browser console, loop over const menu = ["soup", "salad", "pasta", "cake"] with an index and print lines like "1. soup" using i + 1 for the number. Then add a second loop that starts at the last index and prints the items in reverse.
Open the JavaScript workspaceCheck your understanding
Given const a = ["x", "y", "z"], what does for (let i = 0; i <= a.length; i++) { console.log(a[i].toUpperCase()); } do?
- Logs X, Y, Z and then throws a TypeError on the fourth pass
- Logs X, Y, Z and then logs undefined
- Logs X, Y, Z and finishes normally
- Throws immediately because the condition can never be satisfied
Show answer
a.length is 3, so i <= a.length allows four passes: i of 0, 1, 2 and 3. The first three log X, Y and Z, then a[3] evaluates to undefined and calling .toUpperCase() on undefined throws a TypeError. Option 2 is tempting because reading a missing index is itself harmless and merely yields undefined, but the crash comes from the method call on that undefined, not from the read.