Learning web development: Loops in JavaScript

[2025-08-23] dev, javascript, learning web dev
(Ad, please don’t block)

This blog post is part of the series “Learning web development” – which teaches people who have never programmed before how to create web apps with JavaScript.

To download the examples, go to the GitHub repository learning-web-dev-code and follow the instructions there.

I’m interested in feedback! If there is something you don’t understand, please write a comment at the end of this page.


In this chapter, we learn how to do things repeatedly in JavaScript.

for-of loops  

Loops are JavaScript statements that execute the same piece of code zero or more times. The most popular loop is for-of:

const arr = ['ready', 'set', 'go'];
for (const elem of arr) { // (A)
  console.log(elem);
}

The code block (in curly braces) that starts at the end of line A is called the body of the loop.

This loop iterates over the array arr:

  • First it assigns arr[0] to elem and runs the body.
  • Then it assigns arr[1] to elem and runs the body.

One execution of the body is called a loop iteration.

This is what’s logged to the console:

ready
set
go

Notation for methods: type.methodName()  

In order to express “method .push() of arrays”, two notations are common:

  1. array.push()
  2. Array.prototype.push()

We’ll use the first notation. The second one stems from the fact that Array.prototype is where the methods are stored that we can invoke on arrays. How exactly that works is beyond the scope of this series, but you can read about it in section “The internals of classes” of the book “Exploring JavaScript”.

array.push()  

The array method .push() appends a value to the end of an array – thereby increasing its length by one:

> const arr = [];

> arr.push('a')
> arr
[ 'a' ]

> arr.push('b')
> arr
[ 'a', 'b' ]

Processing an array via array.push() and a loop  

One common technique for processing an array is:

  • The output array is initially empty.
  • We loop over the input array. Per iteration, we push zero or more elements to the output array.

The following function is an example:

const extractNonEmptyStrings = (strs) => {
  const result = [];
  for (const str of strs) {
    if (str.length > 0) {
      result.push(str);
    }
  }
  return result;
};

This is the function in action:

> extractNonEmptyStrings([''])
[]
> extractNonEmptyStrings(['yes', '', 'or', '', 'no'])
[ 'yes', 'or', 'no' ]
> extractNonEmptyStrings(['hello'])
[ 'hello' ]

array.join()  

In the next project, we’ll need the array method .join(). It creates a string by concatenating (combining) all the strings in an array into a single string. It puts the string provided as an argument between the elements.

> ['a', 'b', 'c'].join(', ')
'a, b, c'
> ['a', 'b', 'c'].join('-')
'a-b-c'

The argument of .join() can also be an empty string:

> ['a', 'b', 'c'].join('')
'abc'

Project: fruits-you-like-fixed.html  

The user interface of this project works as follows:

The input comes from an unordered list with checkboxes for fruits (only one <li> is shown below):

<ul>
  <li>
    <label>
      <input type="checkbox" value="apples"> apples
    </label>
  </li>
  <!-- Etc. -->
</ul>

The output goes to the following paragraph: text that lists the fruits that were selected.

<p>
  You like: <span id="feedbackSpan"></span>
</p>

Ever time a checkbox is selected or deselected, the following function is called:

const updateFeedback = () => {
  const selectedFruits = [];
  for (const i of document.querySelectorAll('input')) {
    if (i.checked) {
      selectedFruits.push(i.value);
    }
  }
  document.querySelector('#feedbackSpan').innerText =
    selectedFruits.join(', ');
};

We first collect the names of all selected (“checked”) fruits and then show them in #feedbackSpan, separated by commas.

How are we going to listen for changes made by the user to the checkboxes? One option is to add a 'change' listener to each checkbox. However, there is an alternative: Each event is first sent to listeners at the source (the HTML element where they originate), then to listeners at the source’s parent (the immediately surrounding HTML element), then to listeners at the parent’s parent, etc. That is called event bubbling. If we want to stop an event from being passed on, we can call event.stopPropagation(). But that is rarely necessary because we rarely have more than one event listener that listens to a given event (at different HTML elements).

In this project, we listen for the checkbox 'change' events at the <ul> element:

document.querySelector('ul')
  .addEventListener(
    'change',
    (event) => {
      updateFeedback();
    }
  );

Assembling text fragments  

In the project fruits-you-like-fixed.html, the <ul> contains a lot of repetitive HTML code. For the next project, we want to create that HTML via JavaScript, with the input being the following array:

const fruits = [
  'apples',
  'bananas',
  'oranges',
  'strawberries',
  'watermelons',
];

In order to do that comfortably, we need to learn two new tools: The += operator and template literals.

The += operator for numbers and strings  

The += is an abbreviation: The following two expressions are equivalent.

myVar += value;
myVar = myVar + value;

We can use this operator for numbers:

> let num = 0;

> num += 1;
> num
1

> num += 1;
> num
2

We can also use this operator for to concatenate strings:

> let str = '';

> str += 'ice ';
> str
'ice '

> str += 'cream';
> str
'ice cream'

Joining strings via +=  

We have already used the array method .join(). We can use += to implement this method ourselves:

const joinStrings = (strs, separator) => {
  let result = '';
  for (const [index, str] of strs.entries()) { // (A)
    if (index > 0) {
      result += separator;
    }
    result += str;
  }
  return result;
};

Let’s use this function:

> joinStrings(['a', 'b'], '-')
'a-b'

This for-of loop in line A does something different: The array method strs.entries() returns a data structure that is called an iterable. It is very loosely similar to an array. Its elements are index-value pairs. Each pair is stored in an array with two elements.

This is how array.entries() works:

> Array.from(['a', 'b'].entries())
[ [ 0, 'a' ], [ 1, 'b' ] ]

Array.from() converts its argument to an array.

const [index, str] provides a different way of assigning to variables (so-called destructuring): The variables index and str get their data from inside a linear data structure such as an array. This is another example of destructuring:

> const [zero, one, two] = ['a', 'b', 'c'];
> zero
'a'
> one
'b'
> two
'c'

The following code shows the same loop, once with destructuring and once without it:

for (const [index, value] of ['yes', 'no', 'maybe'].entries()) {
  console.log(index + '. ' + value);
}
console.log();
for (const pair of ['yes', 'no', 'maybe'].entries()) {
  console.log(pair[0] + '. ' + pair[1]);
}

It should be obvious that the first loop – which uses destructuring – has more convenient syntax. The complete output is:

0. yes
1. no
2. maybe

0. yes
1. no
2. maybe

Template literals  

A template literal provides us with another way of creating strings that is similar to string literals. Where the content of string literals is delimited by single quotes or double quotes, the content of template literals is delimited by backticks:

const str = `This is a template literal!`;

Template literals have one feature that string literals don’t have: It lets us interpolate (insert) values:

> const num = 99;
> `${num} bottles of juice on the wall`
'99 bottles of juice on the wall'

Template literals with multiple lines  

const text = `First line
Second line
Third line`;
console.log(text);
First line
Second line
Third line

Problem with multi-line template literals  

const words = ['eeny', 'meeny'];
let str = '';
for (const word of words) {
  str += `
    <p>
      ${word}
    </p>
  `;
}
str = str.replaceAll(' ', '·'); // (A)
str = str.replaceAll('\n', '⮐\n'); // (B)
console.log(str);

In this code we first assemble the string str. Then we convert invisible characters to something we can see in the output. The string method .replaceAll() helps us do that:

> 'baaab'.replaceAll('a', 'x')
'bxxxb'

What are we replacing?

  • Line A: We replace each space with a middle dot (·)
  • Line B: In strings \n stands for newline, a character that ends the current line and is also called line break or line terminator. We don’t remove newlines, but we add visible symbols before them so that we can see them more clearly.

The output of the code looks like this:

⮐
····<p>⮐
······eeny⮐
····</p>⮐
··⮐
····<p>⮐
······meeny⮐
····</p>⮐
··

That’s not ideal:

  • The content is indented by four spaces.
  • There are too many newlines.
  • The two spaces in the empty lines at the ends of the template literal strings aren’t great either.

It’s clear why this happens: We want our template literal to look nice, which is why we added newlines and indented it.

The following code shows one way of producing better output:

const words = ['eeny', 'meeny'];
let str = '';
for (const word of words) {
  str += `
<p>
  ${word}
</p>
  `.trim();
  str += '\n'; // (A)
}
str = str.replaceAll(' ', '·');
str = str.replaceAll('\n', '⮐\n');
console.log(str);

This code gives us the following nice output:

<p>⮐
··eeny⮐
</p>⮐
<p>⮐
··meeny⮐
</p>⮐

What’s different in this version?

  • We are not indenting the content of the template literal. The downside is that this looks a lot less nice.

  • We are using the string method .trim() to remove all whitespace (spaces, tabs, newlines etc.) from the beginning and the end of the string created by the template literal.

    > '\n    abc   \n   '.trim()
    'abc'
    
  • Because we removed the newlines at the beginning and the end of the template literal, we manually insert a newline after each template literal string (line A).

Another way of producing better output is via a library (code written by someone else that we can use) such as string-indent. How exactly this library works is beyond the scope of this series; you can read more about a similar library in section “Multiline template literals and indentation” of the book “Exploring JavaScript”.

Project: fruits-you-like-generated.html  

fruits-you-like-generated.html is almost like fruits-you-like-fixed.html but its checkboxes are not created via fixed HTML. Its <ul> is empty:

<ul>
</ul>

The <li> elements are created via the following JavaScript code:

const fruits = [
  'apples',
  'bananas',
  'oranges',
  'strawberries',
  'watermelons',
];

const ul = document.querySelector('ul');
for (const f of fruits) {
  const html = `
    <li>
      <label>
        <input type="checkbox" value="${f}"> ${f}
      </label>
    </li>
  `;
  ul.insertAdjacentHTML('beforeend', html); // (A)
}

In the previous section, we saw that writing the template literal assigned to html like this produces a slightly ugly string. However, in this case, it doesn’t matter because HTML is very forgiving.

In line A, we used the method .insertAdjacentHTML() of HTML elements to insert HTML code into the web page as if we had written it by hand. It is inserted before the end of the <ul>

Terminology: static HTML vs. dynamic HTML  

  • Static HTML is what we called “fixed HTML”: It’s HTML that we write by hand.
  • Dynamic HTML is HTML that is created via JavaScript code. Even us using .innerText for status messages is dynamic HTML: The HTML changes dynamically, usually multiple times while we use an app.

Leaving a loop early via break  

Consider the following functionality:

const firstStrStartingWithA = (strs) => {
  for (const str of strs) {
    if (str.startsWith('A')) {
      return str; // (A)
    }
  }
  return undefined;
};

In line A, we leave the loop as soon as we have found a suitable word. However, once we do that, we also leave the current function and the code after the loop is not executed.

break lets us leave a loop and continue execution after the loop. As an example, the following equivalent implementation of firstStrStartingWithA() uses break:

const firstStrStartingWithA = (strs) => {
  let result = undefined;
  for (const str of strs) {
    if (str.startsWith('A')) {
      result = str;
      break;
    }
  }
  return result;
};

Exercise (without solution)  

  • Rewrite fruits-you-like-generated.html so that it only allows one selection:
    • The app should ask “What is your favorite X?” where X is “animal”, “season”, etc.
    • Use <input type="radio"> and make sure that the HTML doesn’t allow users to select more than one radio button (MDN explains how to do that).
    • Only retrieve the first selected value, not all of them. break may help you here. Assign that value to a variable, don’t push it into an array. Benefit: When updating the feedback, you don’t need to .join() an array.