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!