Let's loop - for...in vs for...of

Laurie - Jul 16 '19 - - Dev Community

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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]]
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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]]
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .