A bit ago I was working with Object.entries and wasn't seeing the functionality I expected. I kept staring, and staring and finally realized I was using "for in" instead of "for of".
And that got me thinking I should write a post to talk about the differences. So here we are!
A Primer
for...in
and for...of
are substitutions for a traditional for loop. It's quite common to need to do something like this.
for (let i = 0; i < arr.length; i++) {
// do something here
}
So the ability to iterate over all kinds of data structures is a nice shortcut.
For...of
for...of
is designed for arrays and other iterables. Here's an example.
let arr = [1, 2, 3]
for (item of arr) {
console.log(item)
}
// 1
// 2
// 3
Keep in mind that a number of things are iterables in JavaScript. This includes arrays, strings, maps, sets, etc.
For...in
On the other hand, for...in
can handle objects.
let obj = {a:1, b:2, c:3}
for (item in obj) {
console.log(item)
}
// a
// b
// c
What's important to note here is that item
is actually referencing the key of a given key-value pair. If we want to access the value we can do something like this.
let obj = {a:1, b:2, c:3}
for (item in obj) {
console.log(obj[item])
}
// 1
// 2
// 3
For...in and iterables
As it turns out, for...in
can handle iterables as well as objects.
let arr = [1, 2, 3]
for (idx in arr) {
console.log(idx)
}
// 0
// 1
// 2
Instead of referencing the key, as it does for objects, it references the index of a given element in the array.
If we want to access the element itself, our code would look like this.
let arr = [1, 2, 3]
for (idx in arr) {
console.log(arr[idx])
}
// 1
// 2
// 3
My wonky example
So it's worth understanding why both versions worked in my example up above and what the difference is.
We'll start with for...of
.
Note that this example uses destructuring assignment and Object.entries, if you want a refresher on those concepts.
For...of
let obj = {a:1, b:2, c:3}
let newObj = {}
for (let [key, value] of Object.entries(obj)) {
newObj[key] = value;
}
// newObj is { a: 1, b: 2, c: 3 }
It might help to break this down a bit. Object.entries()
is turning our obj
into a multidimensional array representation.
[[a,1], [b,2], [c,3]]
As we iterate through that array we're looking at each element, which is an array itself.
From there, we're diving down a level, into that array element, and assigning the name key
to the first element and value
to the second.
Finally, we add those key-value pairs to newObj
. This seems to work as intended.
So what happens with for...in
?
For...in
let obj = {a:1, b:2, c:3}
let newObj = {}
for (let [key, value] in Object.entries(obj)) {
newObj[key] = value;
}
// newObj is { 0: undefined, 1: undefined, 2: undefined }
Uhhh, what?! Let's break this down.
As an aside, now you see why this was not at all the result I was expecting.
So just like before, Object.entries()
is giving us this.
[[a,1], [b,2], [c,3]]
However, as we iterate through the array, we're looking at the array index not the value. So our first entry is 0
, which has no [key, value]
to destructure. key
becomes 0
and value
is given a value of undefined
.
Rabbit hole
Ok, we'll get back to the main point in a second, but I went down a deep rabbit hole trying to understand why this even worked. If we were to break it down to the most basic level, this is the code we're looking at.
const [key, value] = 0;
And that's not valid! It throws TypeError: 0 is not iterable
. So why is this the result when using for...in
?
// key is 0
// value is undefined
Taken from the mozilla docs this is why:
"Array indexes are just enumerable properties with integer names and are otherwise identical to general object properties."
Instead of 0
being of type number
as it is in our const
example, it's actually a string!
So our super drilled down example of what is happening inside the [key, value]
destructuring is really this.
let num = 0;
const [key, value] = num.toString();
// key is '0'
// value is undefined
Ok, back to the point
If we are using for...in
in my example and we want what I was expecting to see, there is a way to get it.
let obj = {a:1, b:2, c:3}
let newObj = {}
for (let idx in Object.entries(obj)){
const [key, value] = Object.entries(obj)[idx]
newObj[key] = value
}
// newObj is { a: 1, b: 2, c: 3 }
However, it's clear that using for...of
is the better choice in this case.
And that's that
It's nice to have so many options, but it's important to pick the right tool for the job. Otherwise, you'll end up with some very unexpected behavior!
Top comments (7)
I realize this is not a post about whether you should use
for loops
or not...I use the AirBNB linting config and they actually error when using either
for...in
orfor...of
. I think it is an interesting choice but, it can help avoid the confusion with the variousfor
methods and the subtle changes in functionality.It is something I have struggled with in some of my code but it seems they prefer you to use Array methods such as
ForEach
,map
,filter
. I think the biggest advantage of avoid the for loops is that it helps you reduce side-effect producing code.The other disadvantage of
for..of
if you are transpiling is that it requiresregenerator-runtime
polyfill.I can't say I've really missed using them in the end even if it was a bit of an adjustment.
All nice points. I don't know that I'm in favor of them or not. I was using them in the example in my tweet because I wanted to really break down what was happening with Object.entries. In production code I don't recall the last time I used them. But either way it was a fun deep dive into the decisions the language makes!
Totally unrelated to your article, which is a great write up of these
for
loops btw, but I always appreciate little optimisations so I hope you don't mind :)It would be more performant to create a reference to
Object.entries
especially when referencing in a loop. As it stands the method will run on each loop which means not only is it running a method to return the entries which is costly, but also allocating memory. Not so bad for small object such as in your example, but large objects may show signs of degradation.Definitely!
Nice article. This was a nice deep dive into the differences between both.
As you mentioned above "As it turns out, for...in can handle iterables as well as objects.", so I wanted to comment on this point.
It's been a while for me to use either of these loops, but I always remember that when I had to choose a loop that I remember reading different articles with general advice to avoid the for..in loop if looping over an array. So, I did a 5 minutes quick search (not anywhere near your deep dive) but this Stackoverflow question has different answers on why a for..in loop is not a good idea for arrays. A clear example of this is if you had undefined values in your array at different indexes, a for..in loop will just ignore the empty values. Anyways, this is worth considering but in the end, the point of which loop to use will depend on the app context.
Thanks for that! my problem is I constantly forget about this stuff and almost always have to revisit the doc :)
Nice breakdown of for...in and for...of loops. It's a nice refresher since I can't remember the last time I used it over its functional counterparts (map, forEach etc)!