loading...
Cover image for When You Should Use JavaScript Maps over Objects

When You Should Use JavaScript Maps over Objects

codeartistryio profile image Reed Barger Originally published at reedbarger.com on ・8 min read

The plain JavaScript object is a great way for organizing our data.

Objects come with limitations, however. Its keys have to be strings (or less frequently used, symbols). What happens if you try to use a non string value for your object keys, such as 1, a number and true a boolean?

const nums = {
  1: 1,
  true: true,
};

Well we can see that both keys are actually converted to strings if we use a special Object method called Object.keys.

Object.keys(nums); // => ['1', 'true']

In this result, we get both of our keys from the object, and as you see they are wrapped in quotes, indicating they are type String.

So there is implicit conversion of keys from whatever value we provide to a string. As a result, we can’t have unique types held as keys on our objects.

In a number of ways, the JS object lacks flexibility and does things we wouldn’t expect. But since the addition of ES6 JavaScript, we have a data type called a map that is often glossed over. Let’s see when to use it:

Why Do We Need Maps?

Think of maps as objects with some extra features. Maps work and were meant to be used just like a normal object, as a key-value storage, but it was created to solve a number of the inherent problems of object.

In this lesson we’re going to dive into when you should use map over plain objects.

The map accepts any key type

The first is the situation we just covered—if the object’s key is not a string or symbol, JavaScript implicitly transforms it into a string.

Maps are special because keys can be any primitive type: strings, numbers, boolean, and symbols. Whatever type we used will be preserved and not implicitly changed to another. This is arguably map’s main benefit.

So since it works just like an object, let’s see how to add values to it.

Unlike the curly braces syntax used to create an object, we create a Map by saying new map.

new Map();

Like object literals, however, we can declare values on it immediately when it’s created. To create these key-values pairs, we include a pair of square brackets:

new Map([]);

And then for each key-value pair, we add an additional set of brackets, that first contains the key and after a comma, it’s corresponding value.

new Map(["key", "value"]);

So let’s put maps to the test, and create our previous object as a map. We’ll make the key for the first pair the number 1, and it’s value 1. And for the second pair, the key will be the boolean true and the value true.

new Map([
  [1, 1],
  [true, true],
]);

Note that like objects, each key-value pair needs to be separated by a comma.

And if we console log this:

console.log(
  new Map([
    [1, 1],
    [true, true],
  ])
);

// Map(2) {1 => 1, true => true}
/* keys are no longer coerced to strings */

We get our created Map. We see that these pairs are totally valid for a map. And like most values in JS, we want to put this map in a variable. We’ll call this map1:

const map1 = new Map([
  [1, 1],
  [true, true],
]);

Now let’s take a look at an alternate way of adding keys and values to a map, particularly after it has been initially created.

Say if we want to add another key-value pair to map1 later in our program, we could use a special method available on every map called .set(). It mutates our map object and the first argument is the key, and the second is the value:

map1.set("key", "value");

// Map(3) {1 => 1, true => true, "key" => "value"}

So let’s add this string to our map to see that all primitives can be added to it as keys.

And then to prove that we our types are being maintained, we can run the following code to get all of the keys.

[...map1.keys()]; // [1, true, "key"]

Here we are using the Map’s .keys() method to get all of the keys of map1, then turning them into array elements using the array spread operator.

As you see, we get a number, boolean, and string. Therefore, in addition to being able to accept keys as whatever primitive we like, did you notice one other thing that map includes, based off of this result? Take a second and see if you can spot it.

The ordered nature of Maps

It might be hard to notice, but look at the order of our keys. It’s exactly the same as we added them. The first two keys are in the same order as we declared then when we created the map and then the last key was added to the end when we used set.

This ordered nature of maps is not present with normal objects. Be aware that normal objects are unordered and the key and values are not arranged in the object according to when they are inserted. However, Maps do preserve insertion order. If you added pairs in a certain order, that will be maintained.

Easier iteration with Maps

Since Map is a newer addition to the language and realizing that iteration is necessary sometimes for objects, a convenient function was built into Maps called that enables us to loop over their data. This is called forEach.

So to iterate over all of our map1 data, we can just say map1.forEach. And forEach is a method that accepts our own function. And most of the time, for when a method accepts a function, we use an arrow function for simplicity’s sake, so our code doesn’t get too cluttered.

map1.forEach(() => {});

And what does forEach do? It gives the function we pass to it the two pieces of data we want. For each pair in the Map, we get its value (that is the first parameter, and then its corresponding key):

forEach will call our function for each individual pair in the map. So to see each data point, we’ll just log the key and value:

map1.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});

So when we run this code, what should we expect to see? What will be first, second and third?

// 1: 1
// true: true
// key: value

We see the key with the number 1 and it’s value, then the boolean key true, and last our string ‘key’.

So again, even in iteration, the order is preserved for Maps. So in one sense, Maps are more flexible due to their ability to store more key data types, but they are also more structured due to maintaining the order we impose on them.

Objects as keys

So let’s dive even deeper into what map can do, that might seem somewhat strange—can you use further an entire object as a key? In fact, you can.

Let’s say we have a couple of objects, for example, a couple set of user data:

const user1 = { name: "john" };
const user2 = { name: "mary" };

And we need to store some important related data with these objects, but we don’t want to attach them to the objects themselves. So for example, say we have a couple of secret keys that are associated with each user, but we want to keep them separate so the users themselves can’t see them.

const secretkey1 = "asdflaksjfd";
const secretkey2 = "alsfkdjasldfj";

To solve this problem with objects isn’t possible. But there’s a workaround: to make our users the keys and their related secret keys as values:

new Map([
  [user1, secretkey1],
  [user2, secretkey2],
]);

And if we call this map secretKeyMap and console.log it:

const secretKeyMap = new Map([
  [user1, secretkey1],
  [user2, secretkey2],
]);
console.log(secretKeyMap);

// Map(2) {{…} => "asdflaksjfd", {…} => "alsfkdjasldfj"}

We see in fact that the user objects were made as keys.

Now there are a couple of downsides to this approach that we should be aware of:

First of all, that it becomes much harder now to access any of the properties off of the keys if we need them. Be aware that such an approach is best when we just need to get the value. What’s incredible about this approach is that all we have to do now to get the secret key of each of the users is just to reference of each user stored in their variables.

And we do this using the opposite of the .set() method to put key-value pairs on maps, .get().

To get the secret key of the first user, we can just say:

const key = secretKeyMap.get(user1);
console.log(key);

And if we run this, we get our associated key. And the same will work for user2:

const secretKeyMap = new Map([
  [user1, secretkey1],
  [user2, secretkey2],
]);
const key = secretKeyMap.get(user2);
console.log(key); // alsfkdjasldfj

Introducing WeakMap

And the second downside is that our objects can be very large and can take up a lot of memory in our application, making it slower. So when we are done using this map, we want it to be garbage collected—that is, thrown away so we can clear up more places in memory for new values.

To do so, we can use a variant of map that is optimized for garbage collection. This is called WeakMap and since it was designed for this purpose, it only accepts objects as keys.

So all we have to do is replace where we used Map with WeakMap and it still works like before:

const key = secretKeyMap.get(user2);
console.log(key); // alsfkdjasldfj

That’s really all you need to know about WeakMap. It works exactly like Map, but use it for situations just like this where there’s a benefit to using objects as keys.

Size of Map

Finally, a significant improvement that Map brings to data that needs to be stored as key-value pairs is that we can easily know how long it is.

You might not be aware of this, but for the normal JS object, there is no length property that tells you how many values it has.

Instead we have to use a trick involving the Object.keys() method we saw earlier. We have to use Object.keys to convert an object to an array of its key values, and then use that array’s length property to see how many data points it has:

const user = {
  name: "john",
  verified: true,
};

console.log(Object.keys(user).length); // 2

The map provides a much more convenient alternative.

For our map, we can either put the key value pairs in it immediately within square brackets, or create and use the set method to dynamically add them. I’ll take the first approach but you can take either:

new Map([
  ["name", "john"],
  ["verified", true],
]);

And remember that since our Object keys are strings, name and verified should be explicitly written as strings with single / double quotes. And I’ll store the created map in a variable called userMap.

const userMap = new Map([
  ["name", "john"],
  ["verified", true],
]);

And now, all we have to do to get the number of key value pairs is use another built-in property to Maps—.size. So if we console log that:

console.log(userMap.size); // 2

We see that has 2 values. Again, if you have data where you need to easily access the number of values that exist in it, you won’t find a better data structure than Map.

Summary

In review, while we will still rely heavily on JavaScript objects do the job of holding structured data, they have some clear limitations:

  • Only strings or symbols can be used as keys
  • Own object properties might collide with property keys inherited from the prototype (e.g. toString, constructor, etc).
  • Objects cannot be used as keys

These limitations are solved by maps. Moreover, maps provide benefits like being iterators and allowing easy size look-up.Objects are not good for information that’s continually updated, looped over, altered, or sorted. In those cases, use Map. Objects are a path to find information when you know where it will be.

In conclusion, use maps with a purpose. Think of maps and objects similar to how let and const are used for our variables. Maps don’t replace objects, they just have their specific use cases. Use objects the vast majority of the time, but if your app needs one of these extra bits of functionality, use map.

Want To Become a JS Master? Join the 2020 JS Bootcamp

Join the JS Bootcamp Course

Follow + Say Hi! 🎨 TwitterInstagramreedbarger.comcodeartistry.io

Posted on by:

codeartistryio profile

Reed Barger

@codeartistryio

Sharing artful coding skills that fuel the life you want to live @ CodeArtistry.io 🎨

Discussion

markdown guide
 

Excellent article! A little correction I would do is that when defining and adding values, new Map expects an array of arrays, so new Map(["key", "value"]); will throw an Uncaught TypeError, it should be new Map([["key", "value"]]);. Again, thank you for explaining it so well!

 

Excellent points. It was asked in one my interview. I didn't know until I read this article

• Only strings or symbols can be used as keys
• Own object properties might collide with property keys inherited from the prototype (e.g. toString, constructor, etc).
• Objects cannot be used as keys
These limitations are solved by maps.
Moreover, maps provide benefits like being iterators and allowing easy size look-up.Objects are not good for information that’s continually updated, looped over, altered, or sorted.

Use objects the vast majority of the time, but if your app needs one of these extra bits of functionality, use map.

 

Should also note that Map operations are not immutable. Set will alter the existing Map rather than creating a new one. This has implications if you plan on using Map in something like Redux state, or functional programming. You'll need/want to write your own operations that do this for you, which may not be performent.

 

Interesting, nice overview.

In most cases where I needed "lookup" I've used objects rather than maps, I guess it's mostly a matter of familiarity (and in some cases compatibility with older JS versions or older browsers, which may not support Map).

An interesting feature (I wasn't aware of that) is the ordered nature of Maps, I can imagine that that can be very useful.

Are there also performance benefits (especially with regards to lookup) of Map versus Object?

 

Yes, there are some performance benefits as compared to objects, for finding, adding, and deleting entries (with .has(), .set(), and .delete()).

If you want to look more into that, check out this article, which covers how much of a performance boost there is from using maps over normal objects.

 

Thanks! Yes I see it, performance is generally a lot better, good to know.

 

Great article! I think it is also worth noting that you can't JSON.stringify a Map object, it produces a string with an empty object literal "{}". So if you need to serialize such a structure, you'd have to do it manually.

 

Objects also tend to be optimized for having a common schema.

V8 will organize objects with the same structure into virtual classes.

Having unique structures is then penalized.

So I'd say that objects for regularly structured data and maps for irregular or unstructured data is probably right.

 

Thanks! This is an awesome overview.

 
 

Going through this amazing bootcamp. Thanks.

 

Great stuff. Thanks for sharing!

 

Map is icing on the cake, and it's not a MUST in any real world app.

 

Should new Map(["key", "value"]); be new Map([["key", "value"]]); (with 2 sets of square brackets)?

 

Thanks for sharing! very helpful

 

Thanks for this very helpful article. Didn't know much about Maps until now.

 

Reed,

Thank you for this article. We have used objects fairly extensively, but after reading your article & seeing the performance differential we will start migrating to Maps.