skip to Main Content

I was trying to swap keys with values in Map and Object.

In Map it goes in an infinite loop, but in Object it goes in a finite loop. I don’t know why is that happening. Please help!

Here is the Code.

const obj = { 1: "one", 2: "two" };
const map = new Map([
  [1, "one"],
  [2, "two"],
]);

for (const [key, value] of map) {  // Goes Infinite Loop
  map.set(value, key);
  map.delete(key);
}
console.log(map); // you will never get this response

for (const key in obj) {  // Goes Finite Loop
 const value = obj[key];
 obj[value] = key
 delete obj[key];
}
console.log(obj); // you will get the response

2

Answers


  1. You mutate the map while iterating thus adding new keys to iterate. So the iteration goes infinite since a new key appears to iterate each iteration cycle. To solve that you need to get a copy of the original keys ahead of the mutation:

    {
        const map = new Map([
            [1, "one"],
            [2, "two"],
        ]);
    
        for (const key of [...map.keys()]) {  // make a copy of keys here
            map.set(map.get(key), key);
            map.delete(key);
        }
        console.log(JSON.stringify(Object.fromEntries([...map])));
    }
    
    // using Map::entries()
    
    {
        const map = new Map([
            [1, "one"],
            [2, "two"],
        ]);
    
        for (const [key, value] of [...map]) {  // make a copy of entries here
            map.set(value, key);
            map.delete(key);
        }
        console.log(JSON.stringify(Object.fromEntries([...map])));
    }

    As I expected the iterating over keys is potentially faster because there’s no overhead to copy all values:

    enter image description here

    <script benchmark data-count="1">
    
        const length = 1000000;
        const map = new Map(Array.from({ length }, (_, idx) => [idx + 1, `value${idx + 1}`]));
    
        // @benchmark using keys
    
        for (const key of [...map.keys()]) {  // make a copy of keys here
            map.set(map.get(key), key);
            map.delete(key);
        }
    
        // @benchmark using entries
    
        for (const [key, value] of [...map]) {  // make a copy of entries here
            map.set(value, key);
            map.delete(key);
        }
    
        // @benchmark using Array.from()
    
        for (const [key, value] of Array.from(map)) {  
            map.set(value, key);
            map.delete(key);
        }
    
    </script>
    <script src="https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js"></script>
    Login or Signup to reply.
  2. The reason why your first attempt results in an infinite loop, is because for...of is iterating the live contents of map. If you add a new key/value pair it will be placed at the end of the iteration cycle (similar to how array.push(item) places the item at the end). This cause an infinite loop.

    Let’s unroll the loop to show exactly what’s going on.

    const map = new Map([
      [1, "one"],
      [2, "two"],
    ]);
    
    for (const [key, value] of map) {  // Goes Infinite Loop
      map.set(value, key);
      map.delete(key);
    }
    
    // 1) key = 1, value = "one"
    map.set("one", 1);
    map.delete(1); //        ▼ next iteration
    // map = Map{ [deleted], 2: "two", "one": 1 }
    //                ▲ current iteration
    
    // 2) key = 2, value = "two"
    map.set("two", 2);
    map.delete(2); //          ▼ next iteration
    // map = Map{ [deleted], "one": 1, "two": 2 }
    //                ▲ current iteration
    
    //  3) key = "one", value = 1
    map.set(1, "one");
    map.delete("one"); //      ▼ next iteration
    // map = Map{ [deleted], "two": 2, 1: "one" }
    //                ▲ current iteration
    
    // 4) key = "two", value = 2
    map.set(2, "two");
    map.delete("two"); //    ▼ next iteration
    // map = Map{ [deleted], 1: "one", 2: "two" }
    //                ▲ current iteration
    
    // 5) key = 1, value = "one"
    map.set("one", 1);
    map.delete(1); //        ▼ next iteration
    // map = Map{ [deleted], 2: "two", "one": 1 }
    //                ▲ current iteration
    
    // infinity ...
    

    Like you can see the iteration can never finish, because you keep adding new entries at the end.

    To solve this issue all you have to do is to make sure you’re iterating over a non-live collection that doesn’t update whenever you change map. This can be done by simply converting the current contents into an array first.

    for (const [key, value] of Array.from(map)) {
      // ...
    }
    

    This will create the array [[1, "one"], [2, "two"]] and iterate over its contents, without it changing during iteration.


    Alternatively you could also invert map by creating a new Map instance.

    const inverted = new Map(Array.from(map, ([key, value]) => [value, key]));
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search