Learn JavaScript

Lesson 4 of 9 ¡ if, switch and Loops

Module 4 ¡ if, switch and Loops

Which Loop, and When switch Beats if

FreeReading

In this lesson

  • Pick a loop from three questions: do you know the count, are you walking every item, and must the body run once before the test.
  • Compare the for loop, the while loop, do...while, for...of, for...in and forEach on one chart, and choose between an if chain, a switch and a lookup object.
  • Stop a loop with break as soon as the answer is known, and explain why forEach cannot.

Amara's code scans a day's orders and flags the first one above 1000. Her loop stops at that order with break. Kenji reviews it and writes one comment: "use forEach everywhere". It sounds tidy, but forEach has no way to stop, so his version would read every order and could flag the wrong one. Both of them know loops. What they lack is a way to choose, and this lesson builds one.

Three questions pick the loop

JavaScript has five everyday loop statements and the forEach method, and most jobs fit more than one of them. Three questions settle it. Ask them in order, and stop at the first yes.

Which loop: a decision flowchart Three questions pick the loop 1. Walking every item of an array or a string? yes for...of gives the values, not the positions no 2. Do you know the count before you start? yes for loop for (let i = 0; i < n; i++) no 3. Must the body run once before the first test? yes do...while the body first, then the test no while loop until something changes, like the input ending An object's keys? for...in with a guard. Module 8 adds Object.keys. Every loop on this chart can stop early with break. forEach cannot.

The questions go from most specific to least. If you walk a list, for...of says so in one line. If you know the count, a for loop keeps the start, the test and the step together. Only when neither holds do you need a while loop, and do...while is the rare case that must run once. So the flowchart always ends at the loop that says the most about your intent.

Six loops on one chart

The flowchart picks a loop for a job. The chart below lists what each loop can do, so you can check a choice. forEach is not a statement but a method of every array, so Module 7 teaches it; it is here because Kenji asked for it.

LoopHas an indexCan breakRuns at least onceWorks on an arrayWorks on an object's keysThe usual job
for loopyes, the counter you makeyesnoyes, by indexnoa known count, or positions
while looponly one you keep yourselfyesnoyes, with a counternorepeat until something changes
do...whileonly one you keep yourselfyesyesyes, with a counternodo it once, then repeat while needed
for...ofno, it gives valuesyesnoyes, its valuesno, a plain object is not iterableevery value of an array or a string
for...inthe keys, as stringsyesnoit runs, but the keys are stringsyesan object's keys (Module 8)
forEachyes, the second parameternonoyesnoevery item, with no early stop (Module 7)

Read the "Can break" column first: five yes and one no. Then "Runs at least once": only do...while, because it tests after the body. Example 1 runs the same list through all six.

Two loop forms new to this lesson

do {
  body
} while (test);

array.forEach((item, index) => {
  body
});
  • do...while runs the body, then checks the test, and repeats while the test is true. Note the semicolon at the end.
  • forEach calls the arrow function once per item, with the item and its index. Module 5 explains functions and Module 7 the method.
  • return inside that arrow function ends one call, not the loop, and break there is a SyntaxError.
Example 1: one list, six loops

Three prices walked six ways. prices.length is how many items the array holds, and prices[i] is the item at position i, counted from 0 (Module 7).

const prices = [120, 450, 1300];
const out = [];

let line = "for:";
for (let i = 0; i < prices.length; i++) {
  line += " " + i + "=" + prices[i];
}
out.push(line);

line = "while:";
let w = 0;
while (w < prices.length) {
  line += " " + prices[w];
  w++;
}
out.push(line);

line = "do...while:";
let d = 0;
do {
  line += " " + prices[d];
  d++;
} while (d < prices.length);
out.push(line);

line = "for...of:";
for (const price of prices) {
  line += " " + price;
}
out.push(line);

line = "for...in:";
for (const key in prices) {
  line += " " + key + " (" + typeof key + ")";
}
out.push(line);

line = "forEach:";
prices.forEach((price, index) => {
  line += " " + index + "=" + price;
});
out.push(line);

console.log(out.join("\n"));
for: 0=120 1=450 2=1300
while: 120 450 1300
do...while: 120 450 1300
for...of: 120 450 1300
for...in: 0 (string) 1 (string) 2 (string)
forEach: 0=120 1=450 2=1300

The for loop and forEach hand you the index; the while loops make you keep one. for...of is the shortest, because the job is "every value". for...in gives the keys, and typeof shows each is a string, not a number. Common mistake 4 shows what that does to a sum.

Run in Compiler

One problem, three ways: if, switch and a lookup

Kenji's cafÊ sells three cup sizes: S is 250 ml, M is 350 ml and L is 500 ml. A program turns a size code into millilitres, and says "unknown" for anything else. Here is that one job written three ways, on the same five codes. The last code, constructor, is Zara's edge case.

Example 2: the if chain

Each test compares the same value with one constant.

const out = [];
for (const size of ["S", "M", "L", "XL", "constructor"]) {
  let ml;
  if (size === "S") {
    ml = 250;
  } else if (size === "M") {
    ml = 350;
  } else if (size === "L") {
    ml = 500;
  } else {
    ml = "unknown";
  }
  out.push(size + ": " + ml);
}
console.log(out.join("\n"));
S: 250
M: 350
L: 500
XL: unknown
constructor: unknown

It works, but size === is written three times. The shape says "one value, many constants", which is exactly what a switch is for.

Run in Compiler
Example 3: the switch

The value is named once, and each case holds one constant.

const out = [];
for (const size of ["S", "M", "L", "XL", "constructor"]) {
  let ml;
  switch (size) {
    case "S":
      ml = 250;
      break;
    case "M":
      ml = 350;
      break;
    case "L":
      ml = 500;
      break;
    default:
      ml = "unknown";
  }
  out.push(size + ": " + ml);
}
console.log(out.join("\n"));
S: 250
M: 350
L: 500
XL: unknown
constructor: unknown

The same output. A reader sees at once that only size is tested. The cost is a break per case, and a missing one is a real bug, which is why linters watch for it.

Run in Compiler
Example 4: the lookup object

The mapping becomes data. { S: 250, M: 350, L: 500 } is an object, a set of named values, and cupMl[size] reads the value named by size; Module 8 teaches objects. Object.hasOwn(cupMl, size) asks whether the object itself holds that name.

const cupMl = { S: 250, M: 350, L: 500 };
const out = [];
for (const size of ["S", "M", "L", "XL", "constructor"]) {
  const ml = Object.hasOwn(cupMl, size) ? cupMl[size] : "unknown";
  out.push(size + ": " + ml);
}
console.log(out.join("\n"));
S: 250
M: 350
L: 500
XL: unknown
constructor: unknown

The loop body is one line, and a new size is one more entry, not one more branch. Zara's edge case is why the guard is there. Every object inherits a few names it never declared, and constructor is one, so cupMl["constructor"] finds a function instead of undefined. Without Object.hasOwn, the last line prints constructor: function Object() { [native code] }.

Run in Compiler

So the rule has three parts. Use a switch for one value against many constants. Use an if chain for ranges and mixed conditions, such as t > 30 or two values at once. Use a lookup object when the mapping is data rather than logic, especially when it may grow or come from a settings file.

Stop as soon as you know

An early exit leaves a loop the moment its answer is known. The prime check from lesson 03 has two of them: break at the first divisor, and the test d * d <= n. Here is what each one saves, counted rather than guessed.

Example 5: counting the divisions

Three numbers near a million, each checked three ways. The counters record how many divisions each way performs.

const out = [];
for (const n of [999983, 999999, 1000000]) {
  let noBreak = 0;
  let isPrime = true;
  for (let d = 2; d < n; d++) {
    noBreak++;
    if (n % d === 0) {
      isPrime = false;
    }
  }

  let withBreak = 0;
  for (let d = 2; d < n; d++) {
    withBreak++;
    if (n % d === 0) {
      break;
    }
  }

  let withRoot = 0;
  for (let d = 2; d * d <= n; d++) {
    withRoot++;
    if (n % d === 0) {
      break;
    }
  }

  out.push(n + (isPrime ? " prime" : " composite") + ": no break " + noBreak + ", break " + withBreak + ", break and root " + withRoot);
}
console.log(out.join("\n"));
999983 prime: no break 999981, break 999981, break and root 998
999999 composite: no break 999997, break 2, break and root 2
1000000 composite: no break 999998, break 1, break and root 1

The two exits help different numbers. break turns almost a million divisions into one or two for a composite, but cannot help a prime, which has no divisor to find. The square-root test helps the prime: 998 divisions instead of 999,981. Together they cover both cases, and neither changes the answer.

Run in Compiler

Now back to Amara's loop. Kenji's forEach would walk all the orders even after the answer is known. A for...of with break stops at it. So does the array method find, which Module 7 teaches and which many teams that prefer methods would use here.

Example 6: the first order above the limit

Amara's loop, with a counter to show how far it reads, and find beside it.

const orders = [120, 450, 1300, 80, 2500, 60];
const limit = 1000;
const out = [];

let looked = 0;
let first;
for (const amount of orders) {
  looked++;
  if (amount > limit) {
    first = amount;
    break;
  }
}
out.push("for...of with break: " + first + ", looked at " + looked);

const found = orders.find((amount) => amount > limit);
out.push("find: " + found);

console.log(out.join("\n"));
for...of with break: 1300, looked at 3
find: 1300

The loop reads three of the six orders and stops. find returns the first item for which the arrow function is true, and it also stops there. Common mistake 2 shows Kenji's forEach version and its real output.

Run in Compiler

Style guides have opinions, and that is fine

A team writes down its habits in a style guide, and a linter enforces them. The Airbnb JavaScript Style Guide, rule 11.1, says not to use iterators and to prefer array methods to for...in and for...of. Its ESLint configuration enforces that with no-restricted-syntax, which forbids both loops, labelled statements and with. The message for for...in warns that it also walks inherited keys.

The message for for...of gives two reasons. Code compiled for older browsers needed a heavy helper library, regenerator-runtime, and the guide prefers array methods anyway. The first reason does not apply to Node 22, which runs for...of natively. The second is a team's taste, and a team is allowed one.

Two ESLint rules from this module are easy to agree with. guard-for-in requires each for...in body to filter its keys with an if, usually Object.hasOwn, so inherited keys are skipped. no-fallthrough reports a case that runs into the next one without a comment saying "falls through". It allows empty grouped cases like Kenji's kiosk in lesson 03, and it is in ESLint's recommended set.

So a rule like Airbnb's is fine. It trades a little choice for code that reads the same across a team. If your team bans for...of, the flowchart still works: its answer becomes find, some or forEach, chosen by whether you need to stop.

Where this is used

  • The Airbnb JavaScript Style Guide. Its iterators rule, 11.1, prefers array methods over loops, and its ESLint configuration reports every for...in and for...of through no-restricted-syntax.
  • ESLint. The guard-for-in rule makes a for...in body filter its keys with an if, so a key the object only inherited does not slip into the loop.
  • Fastify's router. find-my-way, the router under the Fastify web framework, keeps one route tree per HTTP method in an object, this.trees[method]. That is a lookup object where a switch on GET, POST and the rest could have been.

Common mistakes

1. A while loop that should have been a for loop.

const out = [];
let day = 1;
while (day <= 7) {
  out.push("day " + day);
  if (day === 5) {
    out.push("weekend next");
    day++;
  }
  day++;
}
console.log(out.join("\n"));
day 1
day 2
day 3
day 4
day 5
weekend next
day 7

No error, and day 6 is gone. The counter is updated in two places, so on day 5 it moves twice. Write for (let day = 1; day <= 7; day++) and never touch day in the body. You will do this when a while loop grows one special case at a time.

2. forEach with a return, expected to stop it.

const orders = [120, 450, 1300, 80, 2500, 60];
let looked = 0;
let first;
orders.forEach((amount) => {
  looked++;
  if (amount > 1000) {
    first = amount;
    return;
  }
});
console.log("first " + first + ", looked at " + looked);
first 2500, looked at 6

No error, and the answer is wrong. return ends one call of the arrow function, and forEach calls it again for the next order, so 2500 overwrites 1300. Writing break there instead fails with SyntaxError: Illegal break statement. Use for...of with break, or find. You will expect return to stop it, because the word sounds like "leave now".

3. A switch on ranges.

const t = 34;
let label;
switch (t) {
  case t > 30:
    label = "hot";
    break;
  case t > 15:
    label = "mild";
    break;
  default:
    label = "cold";
}
console.log(t + " is " + label);
34 is cold

No error, and nothing matches. Each case is worked out first, so t > 30 becomes true, and the switch asks whether 34 === true. Ranges belong in an if chain. You will try this because "switch beats if" sounded like a rule for every chain.

4. for...in where for...of was meant.

const scores = [10, 20, 30];
let total = 0;
for (const s in scores) {
  total += s;
}
console.log("total " + total);
total 0012

No error, and the total is text. for...in gives the keys "0", "1" and "2", not the scores, and 0 + "0" joins text. Write for (const s of scores). You will mix them up because "in" reads like plain English for "each item in the list".

Brain teaser

const tokens = [];
let at = 0;
let command = tokens[at++];
while (command !== undefined && command !== "quit") {
  console.log("menu: add, list, quit");
  command = tokens[at++];
}

Bob's tool must show its menu at least once, then read commands until quit or the end of the input. On empty input, as here, it prints nothing. Which loop from the chart fixes it with a single console.log line? With your fix, how many menus print for the tokens add, list, quit, add?

Look at the chart's "Runs at least once" column. Then trace your fix by hand: the menu prints, one command is read, and only then does the test run.

Exercise 1Easy

Zara wants the first day of frost in a long weather log, and no more reading after it.

Input. n, then n whole numbers, the temperatures of days 1 to n.

Output. day d for the first day whose temperature is below 0, or none when there is no such day.

Constraints. 1 <= n <= 100000; -100 <= each temperature <= 100.

Sample. Input 6, then 4 2 0 -3 5 -1, gives day 4.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Not graded on its own. A temperature of exactly 0 is not frost, and the flowchart tells you which loop to start from.

Run in Compiler
Exercise 2Medium

Kenji's shop till reads commands until the input ends. sale x adds x to the total and refund x subtracts x. total prints total and the total. close prints closed and the total and ends the run, so every command after it is ignored. Any other word prints unknown and that word, and no amount follows it.

Input. Commands, separated by spaces or line breaks, until the input ends.

Output. One line for each total, close and unknown word, as above. If the input ends without close, print no close at the end. The total starts at 0 and may go below 0.

Constraints. At most 100000 commands; every amount is a whole number from 1 to 1000000; a command word is lower-case letters only.

Sample. Input sale 250 sale 100 refund 50 total tip close sale 5 gives total 300, unknown tip and closed 300 on three lines.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Graded as till-commands. The hidden tests include runs with no close, commands after close, and totals below zero.

Run in Compiler
Exercise 3Hard

Kenji's cinema keeps a seat map: r rows of c seats, 0 for free and 1 for taken. Alice wants the first free seat, reading row by row from the front. Kenji wants to know how many seats were checked to find it.

Input. r and c, then r lines of c numbers, each 0 or 1.

Output. Two lines. First row R seat S for the first free seat, both counted from 1, or full when there is none. Then checked K, the number of seats looked at, the free one included.

Constraints. 1 <= r <= 1000; 1 <= c <= 1000.

Sample. Input 3 4, then 1 1 1 1, 1 1 0 1 and 0 0 0 0 on three lines, gives row 2 seat 3 and checked 7.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Not graded on its own. Solve it twice: with nested loops and a labelled break from lesson 02, then with one loop over all r times c seats. Which one reads better?

Run in Compiler

Common doubts

  • Is forEach slower than a for loop?

    Kenji's favourite question again. It can be, and lesson 05 measures it on Node 22 instead of guessing. For most code the difference is too small to matter, so choose by whether you need to stop.

  • Is there any way to stop a forEach early?

    Only by throwing an error out of it, which turns a normal search into an error path. If you need to stop, the job is not "every item", so use for...of with break, or find and some.

  • Can I write switch (true) to test ranges?

    It runs: each case t > 30 is compared with true, so the first true case wins. But most readers expect a switch to test one value against constants. An if chain says the same thing with no trick.

  • Why not always use a lookup object?

    A lookup maps a value to a value. When each case must do something different, or test a range, the logic belongs in an if chain or a switch. Module 8 shows lookups that hold functions, which blur that line.

Key takeaways

  • Three questions pick the loop: a list means for...of, a known count a for loop, "once before the test" do...while, and anything else a while loop.
  • Every loop statement can break; forEach cannot, and a return inside it ends only one call.
  • for...in gives keys as strings: use it for an object's keys with a guard, never to walk an array.
  • Use a switch for one value against constants, an if chain for ranges, and a lookup object when the mapping is data.
  • Exit early once the answer is known: break saves work on a hit, and a tighter test such as d * d <= n saves it when there is no hit.
  • Go deeper: Under the Hood, loops as jumps, the iterator protocol and a loop lab (Pro).

Next, lesson 05 opens the engine: the loop as a jump in bytecode, the protocol behind for...of, and a loop lab with measured numbers.

End of lesson 4

Mark it done, and your progress moves with you.

Next: Under the Hood: Loops as Jumps, the Iterator Protocol and a Loop Lab