Loops in JavaScript

Looping is a classic programming habit, a way of minimizing repetitive work. Say we needed to count ascending numbers from 0 to 10. Here is the very manual way of doing that:

console.log(0);
console.log(1);
console.log(2);
console.log(3);
console.log(4);
console.log(5);
console.log(6);
console.log(7);
console.log(8);
console.log(9);
console.log(10);

// Expected result: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

This works, but it's incredibly repetitive and doesn't scale well. What if we needed to count to 1000 or 1000000? Looping is a form of control that allows our code to do repetitive tasks with fewer lines of code. There are a handful of different types of loops in JavaScript. In this post I'm going to try to break down how each one works, and when you might use them.

while Loop

Let's see how a loop can help us with the above situation:

let counter = 0;
let upperLimit = 10;

while (counter <= upperLimit) {
  console.log(counter);
  counter += 1;
}
// Expected result: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

The above is much cleaner to look at. We are initially setting a variable named counter to the value of 0, and another variable named upperLimit to the value of 10. The while loop takes a conditional statement in parenthesis, in this case counter <= upperLimit, and then executes the code in the following {} curly bracket block whenever that condition is met.

Starting from the beginning, the loop compares the value of counter, 0, to the value of upperLimit, 10. 0 is indeed less than or equal to 10, so we print out the value of counter, then increment the value of counter by 1 and run the loop again. counter is now 1, which is still less than or equal to 10, so we print the value again and increase the value of counter to 2, and so on. This runs in the same manner for a while, so let's fast forward to the point in the loop where counter's value is 9. At that point, 9 is still less than or equal to 10. We print out 9 and increment counter again. Now counter is 10, which is equal to the value of upperLimit. We print out 10, then increment counter one last time. Since counter's value is now 11, and is no longer less than or equal to the value of upperLimit, 10, the condition is no longer met, so the loop stops running.

It's a lot to write out what happens for each iteration of the loop, but I think it illustrates the point: a while loop takes the hard work out of something that was otherwise a manual process. I did some extra abstraction by creating the upperLimit variable, but this would allow us to easily adjust the value we want to count to. If we want to count to 1000 or 1000000, all we would need to do is change the value of upperLimit to that number.

while Loop Definition and Syntax

From MDN: The while statement creates a loop that executes a specified statement as long as the test condition evaluates to true. The condition is evaluated before executing the statement.

while (condition) {
  statement
}

while Loop Example

While the above situation is good for illustrating the mechanics of a while loop, I don't necessarily like abstract examples and prefer to break things down to something more practical or commonly run into in the real world.

One practical example similar to the above would be hitting the Start button on a stop watch. It begins counting, then doesn't stop until the Stop button is selected. Here's some sloppy pseudocode to illustrate:

while (counting === true) {
  seconds++;
}

start button => set counting to true
stop button => set counting to false // stops the while loop

do...while Loop

The do...while Loop is another looping form of control that is similar to the while Loop, but it differs in that the do {} block will always run at least once before the condition is evaluated. What does this mean? Well, first let's look at the same "count from 0 to 10" problem, addressed with a for...in Loop:

let counter = 0;
let upperLimit = 10;

do {
  console.log(counter);
  counter += 1;
} while (counter <= upperLimit);
// Expected result: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

With the code written as-is, we get the expected output of counting sequentially from 0 to 10 (or whatever the value of upperLimit is). So what's the difference then? Well let's set the value of upperLimit to be -1. Since 0 is greater than 1, it shouldn't count anything out, right?

let counter = 0;
let upperLimit = -1;

do {
  console.log(counter);
  counter += 1;
} while (counter <= upperLimit);

console.log(counter);
// Expected result: 0, 1

So indeed it first counts out 0, and then after the loop if we check the value of counter, its value is 1. That's because the items within the do {} code block will always run at least once, even if the condition is never met. It's in that initial step that both the 0 gets printed to the console and counter's value gets incremented.

do...while Definition and Syntax

From MDN: The do...while statement creates a loop that executes a specified statement until the test condition evaluates to false. The condition is evaluated after executing the statement, resulting in the specified statement executing at least once.

do {
  statement
} while (condition);

do...while Loop Example

The do...while Loop is a little harder for me to come up with examples for. One mental model that may be helpful is if you need to show a prompt that asks for a user's response, and then waits for that user's response.

do {
  show window with input field;
} while (input field is empty -- keep showing input field);

show result of input field after submission.

for Loop

So far we've looked at two different loops and how they can count up from 0 to 10. In both cases, there was an initialization step: we set a counter to 0. Then there was a step where we checked against a condition: is the value of counter less than or equal to 10? And then a final bit of work that gets done if that condition is met: Oh it's still less than or equal to 10? Then let's add 1 to counter. These are the fundamental steps of the basic loops we've been looking at. These steps emerged as such a common pattern that something called the for Loop was written to embrace it. Let's take a look:

for (let counter = 0; counter <= 10; counter += 1) {
  console.log(counter);
}
// Expected result: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

In three lines we accomplished the same task that the above examples did, and we didn't have to create a separate counter variable that lived outside of the counting operation. Furthermore, everything related to the three steps outlined above is all grouped together in the parenthesis, separated by semicolons.

The first statement, let counter = 0, is called the initialization, which gets evaluated before the loop begins. The second statement, counter <= 10, is called the condition, and is the expression that gets evaluated both before the loop begins and on each iteration of the loop. Once the condition evaluates to false, the loop no longer runs. The third statement, counter += 1, is called the final-expression, which is the operation that happens right before the next iteration of the loop. What follows in the {} curly brackets block is called the statement. This is any work that needs to be done when the condition evaluates to true and before the final-expression is called.

for Loop Definition and Syntax

from MDN: The for statement creates a loop that consists of three optional expressions, enclosed in parentheses and separated by semicolons, followed by a statement (usually a block statement) to be executed in the loop.

for ([initialization]; [condition]; [final-expression]) {
  statement
}

Optional Expressions

It's worth noting that any of the three expressions (initialization, condition, and final-expression) can be omitted depending on the work you're trying to do. All you need are the semi-colon ; delimiters.

For example, you may have an initialized value elsewhere in your code that you can use without defining a variable in the initialization.

var initializationOfSomeData = 25;

// other code

for (; initializationOfSomeData < 100; i+= 25) {
  // do some work
}

Or maybe you need to do the comparison inside of the the statement code block, rather than in the traditional condition expression location. It's important that you include some kind of break statement if you do this, otherwise you will write an infinite loop!

for (let i = 1; ; i+= 1) {
  console.log(i);
  if (i % 2 === 0) {
    break; // important!!!
  }
}
// Expected result: 1, 2

The same goes for the final-expression. You simply need to make sure that you're modifying one of the things being compared so that the condition is eventually met and the loop doesn't just run forever and ever.

for Loop Example

I won't spend a ton of time on for Loop examples. If you spend any significant amount of time programming, you'll run into them everywhere. One very common task is looping over every item in an array to do some work:

const names = ['Joey', 'Bart', 'Franklin', 'Sebastian', 'Crawfish'];

for (let i = 0; i < names.length; i += 1) {
  console.log(`Hi, my name is ${names[i]}!`);
}
// Expected results:
// "Hi, my name is Joey!"
// "Hi, my name is Bart!"
// "Hi, my name is Franklin!"
// "Hi, my name is Sebastian!"
// "Hi, my name is Crawfish!"

for...in Loop

The for loop is great for arrays that are indexed numerically, but what if you need to iterate over an object? That is where the for...in loop comes in. Specifically, the for...in loop will iterate over an object's properties whose enumerable value is true, otherwise known as an enumerable property. What does this mean? Let's take a look at the following:

const person = {
  firstName: "Joey",
  lastName: "Reyes",
  age: 31,
  hungry: true,
}

console.log(Object.getOwnPropertyNames(person));
// Expected result: ["firstName", "lastName", "age", "hungry"]

Object.defineProperty(person, "hairColor", {
  value: "Black",
});

console.log(Object.getOwnPropertyNames(person));
// Expected result: ["firstName", "lastName", "age", "hungry", "hairColor"]

for (let property in person) {
  console.log(property);
}
// Expected result: firstName, lastName, age, hungry

console.log(person.propertyIsEnumerable('hairColor'));
// Expected result: false

There's a lot going on here so let's break it down. To start, we define an object person, with a few properties: firstName, lastName, age, and hungry.

After that, we use the Object.getOwnPropertyNames() method to write out all of the object's properties (enumerable and non-enumerable) to the console. The result here is the four properties previously listed. Nothing major.

In the next few lines, we use the Object.defineProperty() method to define a new property called hairColor with the value of "Black" on the person object. This method is a little bit different than the typical shorthand way of creating a new property in that it gives you more fine-tuned control over details of the property's descriptor. These are details like whether the property is configurable or not, whether the property is writable or not, or whether the property is enumerable or not. By default, properties written using the Object.defineProperty() method are not enumerable.

After defining the new hairColor property, I once again use the Object.getOwnPropertyNames() method, and we see the four original properties on the object, as well as the new hairColor property that we just added.

Finally, we get to the for...in Loop. In this case we are just asking the loop to console.log out the names of enumerable properties. firstName, lastName, age, and hungry appear in the console as a result of this loop. Because hairColor's enumerable value is false (again, this is done by default when we use the Object.defineProperty() method), it does not show up in the results of the for...in loop.

We drive the point home by also detecting whether hairColor is enumerable using the propertyIsEnumerable() method, which shows false.

This enumerability side quest is not something that I've commonly run across, but it is important to understand. For a deeper dive into object property descriptors, check out the Property Descriptors section of You Don't Know JS: this & Object Prototypes by Kyle Simpson.

for...in Loop Definition and Syntax

From MDN: The for...in statement iterates over all enumerable properties of an object that are keyed by strings (ignoring ones keyed by Symbols), including inherited enumerable properties.

for (let variable in object) {
  statement
}

for...in Loop Examples

In most cases you likely won't need to worry about enumerability, and the for...in loop is adequate if you need to iterate over properties in an object.

Let's count to 10:

const numbers = {
  a: 0,
  b: 1,
  c: 2,
  d: 3,
  e: 4,
  f: 5,
  g: 6,
  h: 7,
  i: 8,
  j: 9,
  k: 10,
}

for (let number in numbers) {
  console.log(numbers[number]);
}
// Expected result: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

Now let's calculate the average of all of the values in the following object:

const numbers = {
  a: 10,
  b: 20,
  c: 30,
};

let total = 0;
let count = 0;

for (let number in numbers) {
  if (numbers.hasOwnProperty(number)) {
    count++;
    total += numbers[number];
  };
}

const average = total / count;
console.log(average);
// Expected result: 20

for...in loops have some interesting behavior when used with objects that shadow other objects. For example:

This behavior also extends to objects that shadow other objects:

const numbers = {
  a: 0,
  b: 1,
  c: 2,
  d: 3,
  e: 4,
  f: 5,
  g: 6,
  h: 7,
  i: 8,
  j: 9,
  k: 10,
}

const moreNumbers = Object.create(numbers);
moreNumbers.l = 11;
moreNumbers.m = 12;

for (let number in moreNumbers) {
  console.log(moreNumbers[number]);
}
// Expected result: 11, 12, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

When we loop through moreNumbers, the for...in loop first returns 11 and 12, which we defined as the l and m properties on the moreNumbers object. But then we get all of the property values from numbers, which is the object that moreNumbers was created from. for...in performs lookups all the way up an object's prototype chain.

Lastly, it should be noted that enumerable property access happens in something of an arbitrary order. To specifically quote MDN, "A for...in loop iterates over the properties of an object in an arbitrary order." Details around this are difficult to find, but it seems that this is a result of specific JavaScript engines implementing this loop in different ways. This arbitrary property order is important though! While the for...in Loop is great for iterating over objects, it could also be used to loop over items in an array or characters in a string. However, these data types typically assign quite a bit of importance to the order of their data, and since you cannot guarantee that the for...in Loop iteration happens in order, it's not advised that you use it for non-object data types. Random order access could really come to bite you there. The question then naturally arises: What's a good loop to use to iterate over something like a string or an array?

for...of Loop

The last loop we will look at in this post is the for...of loop, which allows you to loop over iterable objects, including strings, arrays, array-like objects (like arguments or NodeLists), TypedArrays, Maps, Sets, etc. In order to be an iterable object, an object must implement the @@iterator method, which follows a protocol defining how the data values in that data type can be looped through. The for...of Loop is nothing more than syntactic sugar for a command to retrieve a data type's Symbol.iterator method and run it.

In fact, to go deeper on this concept, let's run a little experiment with the Symbol.iterator method that comes with a typical array:

// Make an array, its contents don't matter:
const myNewArray = ["Zero", 22, ["a", "B", "cdef"]];

// Get the iterator symbol
const arrayIterator = myNewArray[Symbol.iterator]();

// Call the iterator symbol's .next() method
console.log(arrayIterator.next());
// Expected result: {value: "Zero", done: false}

// Call the iterator symbol's .next() method again
console.log(arrayIterator.next());
// Expected result: {value: 22, done: false}

// Call the iterator symbol's .next() method again
console.log(arrayIterator.next());
// Expected result: {value: ["a", "B", "cdef"], done: false}

// Call the iterator symbol's .next() method again
console.log(arrayIterator.next());
// Expected result: {value: undefined, done: true}

I'm not doing anything spectacular here, just storing the array's Symbol.iterator (or @@iterator) method into a variable, that way I can easily call it. This @@iterator has a .next() method that, when called, steps through each of the values of the array. .next() returns an object with two properties: the value of the current array item and done. If there are more items in the array then done evaluates to false and the @@iterator continues to run. If there aren't any more items in the array then done evaluates to true and the @@iterator stops running.

This is all to illustrate exactly what a Symbol.iterator is: a defined way to step through a data type. Something this structured doesn't exist for an object. But I'm sure something similar to the above does exist for a string or a map.

All of that to say that for...of Loops are the way to go when you need to iterate through a string or an array, and the foundation of this looping control is in the data type having a Symbol.iterator method.

for...of Loop Definition and Syntax

From MDN: The for...of statement creates a loop iterating over iterable objects, including: built-in String, Array, array-like objects (e.g., arguments or NodeList), TypedArray, Map, Set, and user-defined iterables. It invokes a custom iteration hook with statements to be executed for the value of each distinct property of the object.

for (let variable of iterable) {
  statement
}

for...of Loop Examples

Let's count from 0 to 10 again.

const numbers = [0, 1, 2, 3, 4, 5, 6, 7, 9, 10];

for (let number of numbers) {
  console.log(number);
}
// Expected result: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

Or otherwise loop through an array and print the values:

const names = ["Joey", "Bart", "Sebastian"];

for (let name of names) {
  console.log(name);
}
// Expected result: "Joey", "Bart", "Sebastian"

This is starting to look very similar to a good old for Loop implementation, but a key difference here is that the for Loop gives you index-based access (i.e., you would need to do names[i] to get the value "Joey" in the previous example) whereas the for...of Loop allows you to access the array's values directly.

Here's one more example of a practical use for the for...of Loop, which is iterating over a NodeList. A NodeList is a DOM collection, or a series of items on a website. It looks very similar to an array, but doesn't come with all of the array methods we know and love. But some browsers have given NodeList a Symbol.iterator, meaning they enable us to use a for...of Loop on them. The following example simply adds a read class to all of the <p></p> paragraph tags within an <article></article> article tag:

const articleParagraphs = document.querySelectorAll('article > p');

for (let paragraph of articleParagraphs) {
  paragraph.classList.add('read');
}

Alternatives to Loops

As the JavaScript language has grown and matured, there isn't as great a need for writing loops. Especially in the Node/React ecosystem, data seems to have standardized around arrays and JSON objects, and these data structures come with various built-in methods that take care of operations that were formerly commonly written out with loops.

For many data collections, you may need to filter based on some condition, or return a selection of data (or even the full data set!) wherein you have done some work on each item in the collection, or take all of the data and return one value from it (like a sum or average). Each of these operations could be done written out with loops (and honestly it's a pretty good exercise to do so) but in this day and age there is likely some array method or object method that would make that kind of work way easier. Need to filter based on a condition? Check out the .filter() Method. Need to do some work on every item in an array and return all of those modified values? That's what the .map() Method is for. Need to distill the values of an array to a sum or average? That's when you turn to the .reduce() Method.

That said, there are some potential trade-offs to be aware of here. For one thing, there is readability. This is highly subjective, but some coders find the explicitness of loops to be much easier to read than methods, while others will prefer how succinct a set of chained array methods operates. Then there is performance. This is not as much of a concern as computers and phones get faster and more powerful, but if you need to support older browsers or devices, then the general rule of thumb I've heard is that old school loops are much more performant than the newer array and object methods. This would also apply if you're doing work on a massive data set (and in that case maybe JavaScript isn't the best language for the job).

Summary

  • Loops simplify repetitive tasks
  • When would you use while vs. do while vs. for loop?
    • If you need to need to iterate a known amount of times, use a for Loop
    • If you you need to iterate as long as a condition is met, use a while or do...while Loop
      • If you need to run the loop at least once, use a do...while
      • If you can skip the entire loop if the condition is never met, use a while Loop
  • When would you use for...in vs. for...of?
    • If you need to iterate over an object, use a for...in Loop
    • If you need to iterate over a string, array, or some other iterable, use a for...of Loop
  • While loops should be understood, often times in modern JavaScript programming there are array methods and object methods that make commmon iterative tasks easier or more eloquent
    • There are trade offs to consider in the way of readability (depends on developer preference) and/or performance (depends on environment or data size)

Sources / Further Reading

This blog post draws from a lot of different sources for examples, clarification, and checking proper use of terminology and application of each of the different loops