← Fundamentals
Guided lesson 6 questions · 5 languages

For loops and iteration protocols

Counting loops, ranges that leave out their end, and loops that read a collection one element at a time.

A for loop either counts, with a start, a test, and an update around its body, or reads a collection one element at a time until there are no more. Counting from 0 while i < n includes 0 and leaves out n. Know what the variable of the loop holds on each pass: a position, a copy of an element, or a way to reach the element.

Before you begin

Why this matters

Most loops in real programs walk through a list of values or count through a range. A loop that starts one place too late, reads positions instead of values, or changes a copy gives a wrong answer and no error. Knowing what the loop variable holds on each pass lets you predict what the loop reads and what it changes.

What this helps you reason about

  • Predict the first and last values a loop works with, and how many passes it makes.
  • Say what the variable of a loop holds on each pass: a position, a copy of an element, or a way to reach the element.
  • Spot a for...in loop that reads the indexes of an array as text instead of its values.

A road trip has a list of days, a distance for each day, and a stop for fuel every 150 km. A for loop repeats a step for each number in a range or for each value in a list. This lesson shows how a TypeScript for loop counts, why for...of and for...in read different things, and what the loop asks for before each pass.

Already know this? The short version
  • for (let i = 0; i < n; i++) { … } is a while loop with its start, test, and update on one line; the range 0 to n includes 0 and leaves out n.
  • for (const x of array) reads the values; for (const k in array) reads the indexes as strings ("0", "1"), which TypeScript accepts wherever a string is allowed.
  • for...of asks an iterator for next() before each pass and stops at done: true; an iterator keeps its position.
  • An element pushed during a for...of loop is read by the same loop, so a loop that pushes on every pass never ends. Collect changes and apply them after the loop.

Go to the final check· Go to the practice tickets

Words in this lesson (6)
for loop
A loop written for (start; condition; update) { body }. The start runs once, the condition is tested before each pass, and the update runs after each pass.
loop variable
The variable that a for loop sets for each pass, such as the counter or the element that the pass works on.
range
The part of a sequence from a start index up to an end index. The start is included and the end is not, so the range from 1 to 3 holds indexes 1 and 2, and its length is end − start.
for...of loop
A loop written for (const x of values) { … } that runs its body once for each value of an array or another iterable object, in order.
for...in loop
A loop written for (const key in object) { … } that runs its body once for each property name of an object. For an array, the names are the indexes as strings: "0", "1", "2".
iterator
An object with a next method. Each call to next gives an object { value, done }: done is false while values remain, and true when there are no more.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Counting

What does a counting for loop do?

A counting for loop puts three parts of a while loop on one line: for (let km = 0; km < 400; km += 150) { … }. The start runs once, before anything else. The condition is tested before each pass, exactly as in a while loop. The update runs at the end of each pass, before the next test. The variable it sets is the loop variable.

It is the same as let km = 0; before a while (km < 400) loop whose body ends with km += 150. One thing differs: a km declared in the for line exists only inside the loop, and each pass gets its own copy of it.

So the loop below checks the fuel at 0, 150, and 300 km. After the third pass, the update makes km 450. 450 < 400 is false, so no check runs at 450, and the program continues after the loop.

main.ts
for (let km = 0; km < 400; km += 150) {
  console.log("fuel check at " + km); // 0, 150, 300
}
console.log("arrived");

A counting for loop is a while loop with its start, test, and update on one line: the start runs once, the test before each pass, the update after each pass.

Coming from Python: There is no range(); a counting loop writes the start, the test, and the update itself: for (let i = 0; i < n; i++).

Precisely: one variable per pass

When the start declares a variable with let, JavaScript makes a new binding of it for each pass and copies the value across before the update. It matters when code from one pass keeps the variable for later, which a later topic on closures covers. A var in the start makes one variable for the whole loop, as TypeScript var, hoisting, and the temporal dead zone showed.

Experiment

Follow a counting loop through its three parts

The start, let km = 0, runs once. The condition is tested before each pass, as in a while loop. The update, km += 150, runs after each pass and before the next test. So the first pass sees 0, and the update after the third pass makes km 450, which fails the test: no check runs at 450. With km < 300, the update makes km 300 after the second pass, 300 < 300 is false, and 300 is left out.

Why it matters Read a counting for loop as a while loop: the start before the loop, the test before each pass, and the update at the end of each pass.

Predict the lines printed, and whether 0 or 150 comes first. Then change km < 400 to km < 300 and predict again.

for (let km = 0; km < 400; km += 150) {
  console.log("fuel check at " + km);
}
console.log("arrived");
What to look for

This debugger often stops on the same line twice; that is normal. At the first stop, km shows not initialized yet: the start has not run. On the for line, km then shows 0. After each pass, the for line shows the old value, then the new one: 150, 300, and finally 450. With 450, the test fails, and the next stop is the arrived line, where km is no longer listed: it exists only inside the loop. After the edit, the run prints fuel check at 0, fuel check at 150, then arrived: the end, 300, is left out.

Expected output

fuel check at 0
fuel check at 150
fuel check at 300
arrived

02 / Ranges

Where does a range start and stop?

A range is the part of a sequence from a start up to an end. The start is included, and the end is not. for (let i = 0; i < 3; i++) gives 0, 1, and 2: three values, which is end − start.

This fits indexes. An array of n elements has the indexes 0 to n − 1, so a range from 0 to n visits each index exactly once. An array is a list of values in order, written [120, 340, 95]. days[i] is the value at index i, and days.length is the number of values, so i < days.length is the end test for every array.

Two mistakes are common. Starting at 1, because days are numbered from 1, leaves out index 0. Ending with <= n goes one past the last index: in TypeScript, days[days.length] gives undefined, and a total becomes NaN without any error. Both are off-by-one errors.

When the start equals the end, the range is empty, and the body runs zero times. So a loop over an empty array needs no special case.

main.ts
const days = [120, 340, 95];
let total = 0;
for (let i = 0; i < days.length; i++) {
  total += days[i]; // i is 0, 1, 2
}
console.log(total); // 555

A range includes its start and leaves out its end: 0 to n gives the n indexes of an array of n elements, and 0 to 0 gives none.

03 / Each element

How does a loop read each element?

Most loops over an array need the values, not the indexes. for (const city of cities) runs its body once for each value, and city holds that value. Each pass gets a new city, so const is fine. With let, you could assign to city in the body, but that changes only the variable, never the array.

for...in looks almost the same and does something else. It walks the property names of an object. The property names of an array are its indexes, written as strings: "0", "1", "2". TypeScript gives the loop variable the type string, so code such as "drive to " + city still compiles and prints drive to 0.

main.ts
const cities = ["Lyon", "Turin", "Nice"];
for (const city of cities) {
  console.log(city); // Lyon, Turin, Nice
}
for (const key in cities) {
  console.log(key); // "0", "1", "2"
}

for...of reads the values of an array; for...in reads its indexes as strings. Use for...of for arrays.

Coming from Python: Python’s for x in list reads values. In JavaScript that loop is for...of; for...in gives the indexes as strings.

Precisely: what else for...in reads

for...in visits every enumerable string-named property, including ones inherited from the prototype and ones added to the array object by hand, and the specification does not fix their order for every object. That is why it is meant for plain objects, not arrays.

Experiment

Read the values, then the keys

for...of asks the array for its values, one per pass. for...in walks the property names of an object, and the property names of an array are its indexes, written as strings.

Why it matters Use for...of to read the elements of an array. When a loop over an array prints 0, 1, 2 or adds up small numbers, look for for...in.

Predict the three lines. Then change of to in, and predict again.

const cities = ["Lyon", "Turin", "Nice"];
for (const city of cities) {
  console.log("drive to " + city);
}
What to look for

At the console.log line, city shows "Lyon", "Turin", then "Nice". At the for line between passes, city shows not initialized yet: each pass gets a new city. After the edit, city shows "0", "1", then "2", with quotes, and the run prints drive to 0, drive to 1, drive to 2. TypeScript accepts the edit, because a key such as "0" is a string, like "Lyon".

Expected output

drive to Lyon
drive to Turin
drive to Nice

04 / Next value

How does a for loop get its next value?

A for...of loop does not count positions. It asks for the next value. An array can hand out an iterator: an object with a next method. days.values() makes one. Each call to next() gives an object such as { value: 120, done: false }, and the iterator moves on. When no values remain, next() gives { value: undefined, done: true }.

Before each pass, for...of calls next(). If done is false, the loop runs its body with value. If done is true, the loop ends. That is the iteration protocol: ask for the next value until the answer says done. Arrays, strings, Map, and Set all follow it, which is why for...of works on all of them.

main.ts
const days = [120, 340, 95];
const it = days.values();
console.log(it.next()); // { value: 120, done: false }
console.log(it.next()); // { value: 340, done: false }
console.log(it.next()); // { value: 95, done: false }
console.log(it.next()); // { value: undefined, done: true }

for...of calls next() before each pass and stops when next() says done: true.

Experiment

Take two values by hand, then let the loop take the rest

it is one iterator over days, with one position. Each next() call moves it on by one. for...of calls next() on the same iterator before each pass, and it ends when next() gives done: true.

Why it matters An iterator remembers its position. A loop over an iterator starts where the iterator is now, not at the start of the collection.

Predict the three lines. Then delete the second console.log(it.next()); line, and predict again.

const days = [120, 340, 95];
const it = days.values();
console.log(it.next());
console.log(it.next());
for (const km of it) {
  console.log("loop got " + km);
}
What to look for

The panel shows it as {}, so the output is the evidence. The first two lines are { value: 120, done: false } and { value: 340, done: false }. At the console.log line in the loop, km shows 95: the loop went on from where the two next calls left the iterator. The run prints loop got 95. After the edit, the loop gets 340 and 95.

Expected output

{ value: 120, done: false }
{ value: 340, done: false }
loop got 95

05 / Changing

What happens when you add elements during the loop?

An array iterator does not copy the array. Each next() reads the element at its position in the array as the array is now. So an element pushed during a for...of loop is still ahead of the iterator, and the loop reads it. If every pass can push one more element, the loop never ends.

The safe way is to collect the new elements in a second array, and push them after the loop.

main.ts
const days = [120, 340, 95];
const extra: number[] = [];
for (const km of days) {
  if (km > 300) {
    extra.push(50); // not days.push(50)
  }
}
for (const km of extra) {
  days.push(km);
}

A for...of loop reads elements pushed during the loop. Collect new elements in a second array, and add them after the loop.

Experiment

Add a day while for...of reads the array

for...of asks the array for its next value before each pass, and the array answers with what it holds at that moment. An element pushed during the loop is there when the loop asks, so the loop reads it too.

Why it matters In TypeScript, a for...of loop reads elements added during the loop. If every pass can add one, the loop may never end.

Predict the numbers printed and the number of days. Then change push(50) to push(350), and predict again.

const days = [120, 340, 95];
for (const km of days) {
  if (km > 300) {
    days.push(50); // a short day after a long one
  }
  console.log(km);
}
console.log(days.length + " days");
What to look for

When km is 340, the if pushes 50, and days shows [120, 340, 95, 50] at the next stop. The loop then reaches the new element: km shows 95, then 50. The run prints 120, 340, 95, 50, then 4 days. After the edit, each 350 that the loop reaches pushes another 350, so the loop never reaches the end. The output fills with 350, and the stepper ends with Stopped after 240 recorded stops.

Expected output

120
340
95
50
4 days

06 / Try it

Can you explain a fresh example?

Work out which values the loop variable takes before you add anything. The next lesson, Function calls, shows how a program runs code that is written somewhere else.

main.ts
const km = [80, 200, 150, 60];
let total = 0;
for (let i = 1; i < km.length; i += 2) {
  total += km[i];
}
console.log(total);

What is printed, and which days are added?

Reveal the reasoning

260. The loop starts at index 1 and adds 2 after each pass, so i is 1, then 3. The next value, 5, fails the test 5 < 4. So the loop adds km[1], 200, and km[3], 60: the second and fourth days. km[0] is never read, because the start is 1, not 0.

Practice tickets

  1. FOR-101 · The canal holiday total shows too few locks → Free
  2. FOR-102 · Hotel prices in the city-break planner ignore the new tourist tax → Sign in · Pro
  3. FOR-103 · The river cruise summary misjudges which days were long → Sign in · Pro

Keep exploring

Break, continue, and early return →

Primary references