DEV Community

Cover image for Let's loop - for...in vs for...of
Laurie
Laurie

Posted on • Originally published at tenmilesquare.com

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

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!

Top comments (7)

Collapse
 
drewtownchi profile image
Drew Town

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 or for...of. I think it is an interesting choice but, it can help avoid the confusion with the various for 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 requires regenerator-runtime polyfill.

I can't say I've really missed using them in the end even if it was a bit of an adjustment.

Collapse
 
laurieontech profile image
Laurie

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!

Collapse
 
jamesthomson profile image
James Thomson

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.

let obj = {a:1, b:2, c:3}
let entries = Object.entries(obj)
let newObj = {}
for (let idx in entries){
    const [key, value] = entries[idx]
    newObj[key] = value
}
Collapse
 
laurieontech profile image
Laurie

Definitely!

Collapse
 
abbadev profile image
Abdulla T • Edited

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.

    const arr =[];

    arr[3] = "Value at index 3";

    const temp1Arr = [];
    const temp2Arr = [];

    for (const i of arr) {
        temp1Arr.push(i);
    }

    console.log("temp1Array after a for of loop", temp1Arr);
   // temp1Array after a for of loop [undefined, undefined, undefined, "Value at index 3"]

    for (const i in arr) {
        temp2Arr.push(arr[i]);
    }

    console.log("temp2Array after a for in loop", temp2Arr);
   // temp2Array after a for in loop ["Value at index 3"]
Collapse
 
amir profile image
Amir Meimari

Thanks for that! my problem is I constantly forget about this stuff and almost always have to revisit the doc :)

Collapse
 
darrenvong profile image
Darren Vong

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)!