JavaScript Weekly: An Introduction to Iteration and Enumerability

Different Approaches to Iteration in JavaScript

Photo by Dan Freeman on Unsplash

One of the most common tasks for a developer is iteration. Sometimes the purpose is to carry out a particular operation with each value in a data structure. Other times the goal is to transform the data structure itself. Still other times the intent is to operate on the data structure as a whole, such as in the summing of a list of values. Whatever the goal, iteration is an important task in coding, and in JavaScript, there are many different tools to approach this task. Today we are going to look at a few of them, and hopefully, gain a better understanding of how and why different approaches work they way they do.


What is Iteration Anyway?

To start out, let’s explore a few ways to iterate over two of the most common data structures in JavaScript: arrays; and, objects. Afterwards, we will investigate each approach to see in more detail how they function.

Here we have a variable called prices, which contains an array of integers. Imagine that you want to log each of the values to the console. You might do so using any of the following approaches.

Each of the above four approaches results in the values 400, 80, 375, and 870 being logged to the console. However, they differ in implementation, as described below.

  1. A simple for loop that increments a counter, i, which is then used as an index to access the various values in the prices array.
  2. A for…in loop, which iterates over the property names in the prices array (which are the same as the indices), which are in turn used to access the values in the prices array.
  3. A for…of loop, which iterates directly over the values in the prices array.
  4. A built-in function, Array.prototype.forEach, which uses a callback function to carry out some operation on each value in the calling prices array.

Let’s see something similar in action with another common data structure, an object. In this case we have an object called products, which has a few property names that correspond to certain products and their associated prices as property values.

In this example, the strings “widget : 400”, “gear : 80”, “crank : 375”, and “lever : 870” are logged to the console twice. We accomplished this with two approaches:

  1. A simple for loop that increments a counter, i, and uses it to iterate over the keys of the products object, which were previously extracted using the Object.keys method, and thus access the values in products.
  2. A for…in loop, which iterates over the property names in the products object and uses them to access the associated values.

Enumerability

The above examples may seem fairly straightforward, but what is happening behind the scenes? The answer depends on the approach being used, but one important concept differentiates certain approaches: enumerability. This is a complicated topic worthy of deeper exploration, but in short, the properties on an object (be it a normal object or an array) have associated internal flags that define their behavior, including an enumerable flag set to either true or false. We can see these flags using the Object.getOwnPropertyDescriptors method:

As you can see, each property has an enumerable flag. So what does it do? Well, certain iteration approaches employ this flag as a kind of instruction regarding whether that property should be iterated over. A for…in loop in particular iterates only over those properties that are marked as enumerable: true. We can manually change the flags using the Object.defineProperty method, which results in a change in iteration behavior.

Note how in the above example, where we change the enumerable flag on the property at index 1 of the prices array to false, the corresponding value of 80 is not logged. The reason for this is that the for…in loop saw that the property with the key of 1 was marked as enumerable: false and thus skipped over it! Conversely, even with enumerable flags set to false, other iteration methods such as the for…of loop or the built-in Array.prototype.forEach method will remain unchanged because they do not depend on this flag for instructions.

The same behavior can be observed in regular objects. Let’s go back to our products object and see what happens if we change one of its properties to enumerable: false.

You will note that in both of our approaches, the product with the key “gear” was not logged. We have seen earlier that changing enumerability alters how a for…in loop functions, but now we can also see that it affects other methods. In the Object.keys method the key of “gear” wasn’t included in our productKeys array and its value was thus never logged.


So what?

So, why does any of this matter? First and foremost, it matters because we, as developers, care how things work “under the hood.” But more pragmatically, it matters because there are nuances in how different iteration approaches operate, and misunderstandings of such nuances can lead to unexpected problems. Consider the following:

Here, we use Object.create to define a new variable, otherProducts, which has the products object as its prototype. We then add one property, “wheel” to the otherProducts object. When we iterate over the keys of otherProducts and log corresponding values, we get an expected result of “wheel: 210”. But what happens if we try to use a for…in loop?

It logs all of the properties on otherProducts and all of those from its prototype, products! That probably isn’t what we wanted is it? This kind of unexpected behavior can be guarded against if you have an appropriate understanding of the associated nuances. In this case, we can set up a conditional clause to only act on a given property if it is directly owned by the object in question:


TL;DR

JavaScript provides a number of ways to iterate over data structures such as arrays and objects, some of which are shared, and some of which are not. Such iteration approaches include for loops, for…in loops, for…of loops, and built-in functions such as Array.prototype.forEach. Although these approaches may appear to produce the same outputs, they operate differently. The for…in loop, for example, relies on the enumerability flags of various properties in the relevant object or array. Changing the enumerability of a particular property results in a change in iteration outcomes. Understanding nuances like these is important in order to avoid encountering unexpected behavior.


And there you have it! A short introduction to iteration and enumerability in JavaScript. And there is a lot more ground to be covered, including the iteration behavior of other iterable objects like Sets and Maps, as well as the use of custom iterators and explicit iteration using the iterable and iterator protocols. I hope this review has helped to clarify some of the nuances associated with iteration, so that as you write future code, you can think critically about how, why, and where to use different iteration approaches.