Skip to the content

Loops with for, for...of, forEach and while

Last updated: By Lukas BrandtLesson 8 of 115 min read

Arrays hold lists, and loops walk through them. A loop runs the same block of code again and again, once per item or until some condition changes. That is how you attach a listener to every button on a page, build a list item for every entry in an array, or keep rolling a die until it shows a six.

The classic for loop

const parts = ['CPU', 'cooler', 'thermal paste'];

for (let i = 0; i < parts.length; i++) {
  console.log(i, parts[i]);
}
// 0 "CPU"
// 1 "cooler"
// 2 "thermal paste"

The parentheses hold three parts, separated by semicolons:

  1. let i = 0 runs once, before the first pass, and creates a counter.
  2. i < parts.length is checked before every pass. As soon as it is false, the loop ends.
  3. i++ runs after every pass and adds 1 to the counter.

Use < with length, never <=. With three items the valid indexes are 0, 1 and 2, and i <= parts.length makes one extra pass with i at 3, which reads undefined. This off-by-one error is the most common loop bug there is.

The classic form earns its extra punctuation when you need the index, want to count backwards (start at parts.length - 1 and use i--), or want to step by more than one at a time.

for...of

When you only need the items themselves, for...of is shorter and harder to get wrong:

for (const part of parts) {
  console.log(part);
}

Each pass puts the next item into part. It works on arrays, on strings (one character per pass) and on NodeLists, so it fits element work well:

const buttons = document.querySelectorAll('.tab-button');

for (const button of buttons) {
  button.addEventListener('click', () => {
    button.classList.toggle('is-active');
  });
}

This is the fix for the NodeList mistake from the events lesson: the list itself has no addEventListener, so you attach a listener to each element in turn. const is fine as the loop variable because every pass gets a fresh one, and each listener remembers its own button.

forEach

Arrays and NodeLists also have a forEach method. It calls your function once per item and passes the item, its index and the whole list:

parts.forEach((part, index) => {
  console.log(`${index + 1}. ${part}`);
});
// 1. CPU
// 2. cooler
// 3. thermal paste

It reads nicely with arrow functions and you will see it in a lot of modern code. The catch is that it cannot stop early. break inside the callback is a syntax error, and return only ends the current call, after which forEach moves straight on to the next item.

while

A while loop repeats for as long as its condition stays true. It suits cases where you cannot know the number of passes in advance:

let roll = 0;
let tries = 0;

while (roll !== 6) {
  roll = Math.floor(Math.random() * 6) + 1;
  tries++;
}

console.log(`Rolled a six after ${tries} tries`);

Math.random() returns a number from 0 up to, but not including, 1. Multiplying by 6, rounding down with Math.floor and adding 1 turns that into a whole number from 1 to 6.

Something inside a while loop must eventually make the condition false. Delete the roll = ... line and roll stays 0 forever: the tab freezes, and after a while the browser offers to stop the page. A do...while loop works the same way but tests at the bottom, so its body always runs at least once.

break and continue

break leaves a loop immediately. continue skips the rest of the current pass and jumps to the next one. Both work in for, for...of and while:

const readings = [52, 58, 0, 61, 97, 64];

for (const value of readings) {
  if (value === 0) {
    continue;  // a missing reading, skip it
  }
  if (value > 90) {
    console.log(`Stopped at ${value}`);
    break;     // no need to look any further
  }
  console.log(value);
}
// 52, 58, 61, then "Stopped at 97"

The 64 at the end is never printed: break ended the loop one item earlier.

Building a list from an array

Loops and createElement together turn data into page content:

const list = document.querySelector('#parts-list');

for (const part of parts) {
  const item = document.createElement('li');
  item.textContent = part;
  list.append(item);
}

createElement makes a new element that is not on the page yet, and append places it inside list, after anything already there. Setting textContent rather than innerHTML means a part name that contains a < appears as text instead of being read as markup.

Questions

Which loop should I use?

For the items of an array or NodeList, for...of. When you also need each item's index, forEach or a classic for. When the number of passes depends on something that happens inside the loop, while. If you need to stop early, anything except forEach.

What about for...in?

for...in walks over the keys of an object. On an array it gives you the indexes as strings ('0', '1', '2') and can pick up extra properties too. Keep it for plain objects and use for...of for arrays.

Why does every listener see the last value?

You are probably reading older code with var i in a classic for loop and listeners inside it. var creates one shared variable for the whole loop, so by the time anyone clicks, every listener sees the final value of i. Declaring the counter with let gives each pass its own copy and the problem disappears.

Parts links can earn us a commission. As an Amazon Associate I earn from qualifying purchases.

Try it

Open the online HTML editor, put three buttons with class="tab-button" on a page and give .is-active a background color. Add the for...of listener loop from above. Then rewrite it with forEach, and once more as a classic for loop using buttons[i], and check that all three versions behave the same.