An Introduction to Iteration and Enumerability in JavaScript
July 08, 2018
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.
var prices = [400, 80, 375, 870];
// 1
for (var i = 0; i < prices.length; i += 1) {
console.log(prices[i]);
}
// logs 400, 80, 375, 870
// 2
for (var k in prices) {
console.log(prices[k]);
}
// logs 400, 80, 375, 870
// 3
for (var v of prices) {
console.log(v);
}
// logs 400, 80, 375, 870
// 4
prices.forEach(function(val) {
console.log(val);
});
// logs 400, 80, 375, 870
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.
- A simple
for
loop that increments a counter,i
, which is then used as an index to access the various values in theprices
array. - A
for…in
loop, which iterates over the property names in theprices
array (which are the same as the indices), which are in turn used to access the values in theprices
array. - A
for…of
loop, which iterates directly over the values in theprices
array. - A built-in function,
Array.prototype.forEach
, which uses a callback function to carry out some operation on each value in the callingprices
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.
var products = {
"widget": 400,
"gear": 80,
"crank": 375,
"lever": 870,
};
// 1
var productKeys = Object.keys(products);
for (var i = 0; i < productKeys.length; i += 1) {
var key = productKeys[i];
console.log(key + " : " + products[key]);
}
// logs "widget : 400", "gear : 80", "crank : 375", "lever : 870"
// 2
for (var product in products) {
console.log(product + " : " + products[product]);
}
// logs "widget : 400", "gear : 80", "crank : 375", "lever : 870"
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:
- A simple
for
loop that increments a counter,i
, and uses it to iterate over the keys of theproducts
object, which were previously extracted using theObject.keys
method, and thus access the values inproducts
. - A
for…in
loop, which iterates over the property names in theproducts
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:
var prices = [400, 80, 375, 870];
var products = {
"widget": 400,
"gear": 80,
"crank": 375,
"lever": 870,
};
var pricesDescriptors = Object.getOwnPropertyDescriptors(prices);
console.log(pricesDescriptors);
// logs:
// { 0: {value: 400, writable: true, enumerable: true, configurable: true},
// 1: {value: 80, writable: true, enumerable: true, configurable: true},
// 2: {value: 375, writable: true, enumerable: true, configurable: true},
// 3: {value: 870, writable: true, enumerable: true, configurable: true},
// length: {value: 4, writable: true, enumerable: false, configurable: false} }
var productsDescriptors = Object.getOwnPropertyDescriptors(products);
console.log(productsDescriptors);
// logs:
// { crank: {value: 375, writable: true, enumerable: true, configurable: true},
// gear: {value: 80, writable: true, enumerable: true, configurable: true},
// lever: {value: 870, writable: true, enumerable: true, configurable: true},
// crank: {value: 375, writable: true, enumerable: true, configurable: true} }
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.
var prices = [400, 80, 375, 870];
Object.defineProperty(prices, 1, { enumerable: false });
for (var k in prices) {
console.log(prices[k]);
}
// logs 400, 375, 870
// Note that the 80 is missing!
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
.
var products = {
"widget": 400,
"gear": 80,
"crank": 375,
"lever": 870,
};
Object.defineProperty(products, "gear", { enumerable: false });
// 1
var productKeys = Object.keys(products);
console.log("productKeys: ", productKeys);
// logs productKeys: [ "widget", "crank", "lever" ]
for (var i = 0; i < productKeys.length; i += 1) {
var key = productKeys[i];
console.log(key + " : " + products[key]);
}
// logs "widget : 400", "crank : 375", "lever : 870"
// 2
for (var product in products) {
console.log(product + " : " + products[product]);
}
// logs "widget : 400", "crank : 375", "lever : 870"
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:
var products = {
"widget": 400,
"gear": 80,
"crank": 375,
"lever": 870,
};
var otherProducts = Object.create(products);
otherProducts["wheel"] = 210;
var otherProductKeys = Object.keys(otherProducts);
console.log("otherProductKeys: ", otherProductKeys);
// logs otherProductKeys: [ "wheel" ]
for (var i = 0; i < otherProductKeys.length; i += 1) {
var key = otherProductKeys[i];
console.log(key + " : " + otherProducts[key]);
}
// logs "wheel: 210"
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?
var products = {
"widget": 400,
"gear": 80,
"crank": 375,
"lever": 870,
};
var otherProducts = Object.create(products);
otherProducts["wheel"] = 210;
for (var product in otherProducts) {
console.log(product + " : " + otherProducts[product]);
}
// logs "wheel: 210", widget : 400", "crank : 375", "lever : 870"
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:
var products = {
"widget": 400,
"gear": 80,
"crank": 375,
"lever": 870,
};
var otherProducts = Object.create(products);
otherProducts["wheel"] = 210;
for (var product in otherProducts) {
if (otherProducts.hasOwnProperty(product)) {
console.log(product + " : " + otherProducts[product]);
}
}
// logs "wheel: 210"
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.
Note: This article was originally published on Medium.