DEV Community

Cover image for Why to use Maps over Objects in JS ?
faisal khan
faisal khan

Posted on

Why to use Maps over Objects in JS ?

In JavaScript, an object is a standalone entity, with properties and type.

Compare it with a cup, for example. A cup is an object, with properties. A cup has color, design, weight, and the material it is made of, etc.


Enough talk


Table Of Contents

1. Problems I faced while working with objects:

  • Only string or symbol could be used as key.

    • Objects have a limitation that their keys have to be strings
       const names = {
         1: 'One',
         2: 'Two',
       };
       Object.keys(names); // => ['1', '2']
    
    • The numbers 1 and 2 are keys in the names object. Later, when the object’s keys are accessed, it turns out that the numbers were converted to strings.
    • Implicit conversion of keys is tricky because you lose the consistency of the types.
  • No proper helper methods to work with objects.

    • In order to find the length of the object we need to either use Object.keys() or Object.values() and then find the length by accessing .length on the array returned.
    • Similarly to iterate over it we have to use the same methods above to perform an iteration over the object.
  • Own object properties might collide with property keys inherited from the prototype (e.g. toString, constructor, etc).

    • Any object inherits properties from its prototype object.
    • The accidentally overwritten property inherited from the prototype is dangerous. Let’s study such a dangerous situation.
    • Let’s overwrite the toString() property in an object actor:
       const actor = {
        name: 'Harrison Ford',
        toString: 'Actor: Harrison Ford' 
       /* 
       this will cause a problem since we are overriding the 
       toString method of the prototype chain
       */
      };
    
  • Deleting keys causes problem in large objects.

    • Using delete causes various forms and magnitudes of a slowdown in many situations, because it tends to make things more complicated, forcing the engine (any engine) to perform more checks and/or fall off various fast paths.

2. Solution: Using Maps Data Structure

Maps is a collection of keyed data items, just like an object. But the main difference is that Map allows keys of any type.

Methods and properties are:

  • new Map() – creates the map.
  • map.set(key, value) – stores the value by the key.
  • map.get(key) – returns the value by the key, undefined if the key doesn’t exist in map.
  • map.has(key) – returns true if the key exists, false otherwise.
  • map.delete(key) – removes the value by the key.
  • map.clear() – removes everything from the map.
  • map.size – returns the current element count.

Code:

let map = new Map();

map.set('1', 'str1');   // a string key
map.set(1, 'num1');     // a numeric key
map.set(true, 'bool1'); // a boolean key

// remember the regular object? it would convert keys to string
// Map keeps the type, so these two are different:
alert( map.get(1)   ); // 'num1'
alert( map.get('1') ); // 'str1'

alert( map.size ); // 3
Enter fullscreen mode Exit fullscreen mode

Maps have useful and intuitive helper methods which are used to perform different operations.

3. Comparison: Objects and Maps

Parameters Object Maps
Iteration Object does not implement an iteration protocol, and so objects are not directly iterable using the JavaScript for...of statement (by default). A Map is an iterable, so it can be directly iterated
Key Types The keys of an object must be either a String or a Symbol. A Map's keys can be any value (including functions, objects, or any primitive).
Size The number of items in an object must be determined manually. The number of items in a Map is easily retrieved from its size property.
Performance Not optimized for frequent additions and removals of key-value pairs. Performs better in scenarios involving frequent additions and removals of key-value pairs.

4. Practical Example

Let's take an example of implementing select all functionality.

Select All

const selectedItems = {}; 
// we will use object here for quick lookup since its search is O(1)

// adding item into selectedItems
selectedItems['image/png'] = true 
selectedItems['text/html'] = true

// removing item from selectedItems
selectedItems['application/pdf'] = false 
Enter fullscreen mode Exit fullscreen mode

The code seems simple, but if you notice we are not deleting the key here we are setting it to false.

Screenshot 2021-10-06 at 1.49.49 PM

So in order to change the header selection state either from partial to complete or vice versa.We need to traverse over the object and detect false and true values.

It would have been easy if we could have deleted items from an object and then had checked the length of the object to determine if the current state is partial or complete.

But delete has performance issues in our V8 engine especially when we want to do multiple deletions of keys.

Maps comes to the rescue, map has delete functionality as well as functionality to return size, unlike object where we need to convert to array and then find the length of it. All without causing performance bottleneck.

const selectedItems = new Map()

// adding item into selectedItems
selectedItems.set('image/png') 
selectedItems.set('text/html')

// removing item from selectedItems
selectedItems.delete('application/pdf')
Enter fullscreen mode Exit fullscreen mode

One of the solutions was to set selectionItems to {} when we want to remove all the selected items, but that is not a scalable solution in certain situations.

When we do pagination in a table we have scenarios where select-all is performed to items specific to the current page and not on the items of the next or previous page.

In this case, if we set selectedItems = {} it will reset all the values, which is an incorrect solution.

Hence, maps are more scalable solution since it does not face any problem with respect to the deletion of the key.

5. Problems in Maps

  • Maps is not here to replace objects

    • If we are only using string-based keys and need maximum read performance, then objects might be a better choice.
    • This is because Javascript engines compile objects down to C++ classes in the background, and the access path for properties is much faster than a function call for Map().get().
    • Adding or removing a property causes the shape of the class to change and the backing class to be re-compiled, which is why using an object as a dictionary with lots of additions and deletions is very slow, but reads of existing keys without changing the object are very fast.
  • Maps are not serializable

6. Conclusion

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

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 maps.

In conclusion, use maps with a purpose. Think of maps and objects similar to how let and const are used for our variables.

Enough talk

Discussion (26)

Collapse
lukeshiru profile image
LUKESHIRU

A few things to consider:

  • Only string or symbol could be used as key: You can simply do:
const names = {
    1: "One",
    2: "Two"
};
Object.keys(names).map(Number); // => [1, 2]
// Or
Object.keys(names).map(parseFloat); // => [1, 2]
// Or
Object.keys(names).map(key => parseInt(key, 10)); // => [1, 2]
Enter fullscreen mode Exit fullscreen mode
  • No proper helper methods to work with objects: You generally don't need the length, but if you do, as you mentioned, you can just use the .length property after doing Object.keys, Object.values or Object.entries.

  • Own object properties might collide with property keys inherited from the prototype: That flexibility ends up being useful as well, because you can customize the toString behavior if you use it properly.

  • Deleting keys causes problem in large objects: IF you know what you're doing, you should avoid mutations such as delete (you can even ban it with ESLint). Mutations are evil.

Now, about the example you showcase of Map, that can be solved without maps. First off, for that particular scenario, being just string->bool, you could instead handle this with arrays:

let selectedItems = [];

// adding item into selectedItems
selectedItems = [...selectedItems, "image/png", "text/html"];
// or with mutation
selectedItems.push("image/png", "text/html");

// removing item from selectedItems
selectedItems = selectedItems.filter(
    selectedItem => selectedItem !== "application/pdf"
);
// or with mutation
selectedItems.splice(selectedItems.indexOf("application/pdf"), 1);
Enter fullscreen mode Exit fullscreen mode

Selections/lists such as the ones in your example should be handled by arrays, not objects or Maps.

Cheers!

Collapse
faisalpathan profile image
faisal khan Author

Thanks for commenting, the answer to your points are as follows:

  1. Only string or symbol could be used as key: but looping over the object to achieve this is what i feel is not ideal scenarios. At the same time as per your solution it is not possible to have one key as number and other key as string in the same object

  2. No proper helper methods to work with objects: In day to day scenarios, i have build multiple features and it was my major requirement where i needed to find the length of the object, moreover helper methods which maps provides easily surpasses all my needs i expected from object.

  3. Own object properties might collide with property keys inherited from the prototype: I agree with you, but this is both curse and boon for me, when we develop a feature we normally do not use toString in-order to customize its behaviour, but a the same time if someone overwrites the methods it can cause problems when it was not intended.

  4. Deleting keys causes problem in large objects: Mutations are evil i agree, but how do you remove a key from a copied object ? i am happy to have a conversation on this

Now coming to the solution you have given to me on the example, the problem with your solution is scalability

As per your solution when we select all the items from the list we have to add all the items in an array of items.

But when we are rendering selected state on each of the row we have to traverse the complete array to find out if the value exists in the selectItems list or not, which is O(n), why this matter is assume you having 1000 rows and you are traversing your selectedList array 1000 times to show selected state on the row

But when we use an objects or maps it is O(1).

Cheers!

Collapse
lukeshiru profile image
LUKESHIRU
  1. If you're using Map because you often need to loop over structures that have different types for keys/values, the problem is not with objects but with the data structures you're building/using. Ideally you should have consistent types for keys and values. Your example with the key "1" and the key 1 in the same Map already shows something that is not possible with objects, which is good. Looping over objects is really easy you can either use Object.entries (my preferred method), or you can use for...in.
  2. Again, generally you don't need to know the length of objects (that's what arrays are for), and if you need methods to loop over them, you have all array methods after you use Object.entries.
  3. I think the entire list of properties you should use with caution is __defineGetter__, __defineSetter__, __lookupGetter__, __lookupSetter__, __proto__, constructor, hasOwnProperty, isPrototypeOf, propertyIsEnumerable, toLocaleString, toString, valueOf ... does it happen often that you need to use some of those names? I mean even your example is weird because you have a toString with a string value instead of being a function.
  4. You can delete keys from a copied object by omitting that key when you do the copy, several ways of doing that:
const object = { foo: 1, bar: 2, deleteMe: 3 };

// Ideal
const spreadDelete = ({ deleteMe, ...rest }) => rest;

// Over complicated, but also works
const entriesDelete = object =>
    Object.fromEntries(
        Object.entries(object).filter(([key]) => key !== "deleteMe")
    );

// Horrible, but still works
const copyDelete = ({ ...object }) => {
    delete object.deleteMe;
    return object;
};

spreadDelete(object); // { foo: 1, bar: 2 }
entriesDelete(object); // { foo: 1, bar: 2 }
copyDelete(object); // { foo: 1, bar: 2 }
Enter fullscreen mode Exit fullscreen mode

About the your example with selected items, if you're working with lists, arrays are the solution. I used strings as examples because you did, but if the state is more complex, you can still use arrays. Lets say you're receiving something like this from the back-end (it happens that even the back-end doesn't understand that they should use array for lists):

{
    "rows": {
        "id-01": {
            "column-1": "foo",
            "column-2": "bar",
            "column-3": "baz"
        },
        "id-02": {
            "column-1": "foo",
            "column-2": "bar",
            "column-3": "baz"
        },
        "id-03": {
            "column-1": "foo",
            "column-2": "bar",
            "column-3": "baz"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Instead of punishing yourself for the bad decisions of the backend and trying to loop over the rows object or trying to turn that into a Map, you should first turn it into an array and then save it in your state. If you need to have something else like "selected", you can just add that to your array items:

const state = Object.entries(response.rows).map(([id, columns]) => ({ id, columns, selected: false }))
Enter fullscreen mode Exit fullscreen mode

And then, being an array, is super easy and efficient to loop over it, update it, map it, and so on. So let's say that you're selecting items to delete, and your backend only takes a list of ids to delete:

const pick = property => object => object[property];

api.delete(state.filter(pick("selected")).map(pick("id")));
Enter fullscreen mode Exit fullscreen mode

Maybe you want to update the selected fields, so the backend needs the entire objects to update, we can turn them back to objects:

api.put(
    Object.fromEntries(
        state.filter(pick("selected")).map(({ id, columns }) => [id, columns])
    )
);
Enter fullscreen mode Exit fullscreen mode

Cheers!

Thread Thread
faisalpathan profile image
faisal khan Author • Edited

Thanks for commenting

  1. The point here was Map solves this purpose, i am not trying to showcase that Objects are inferior to Maps, but Maps are designed to solve some loopholes which were initially created, and Maps does it without looping over the keys

  2. We need length of objects, it a simple clash between what you feel is ideal and what is real, ideal world !== real world. Why i wrote the problem is because multiple dev would have faced it, and its not wrong to do it with .entries or .keys, but if we have a data structure which does not allow you doing any of those stuffs and gives to the length in o(1) then why not use it in such condition ?

  3. It was a point to consider, some devs already know it, but there can be problem if someone is not aware of the inherited methods, hence my point. Again Maps does not have this, its pretty safe to use was my answer to this.

  4. I liked the way the spread works, i forgot about it, its a good point, how this this work if i want to delete nested keys ?

Now coming to your another solution for the example. Here we have no backend dependency. Backend will always give us array of objects to render as rows in the table.

The point here is what happens when you select a row or what happens when you click on select all on the header. You are using a array to maintain selected items which is correct no issues in small or moderate list.

My point over here is when re-rendering the list again, post select-all is clicked, every row while rendering will have to make a lookup to the selected list array, to know if we want to render a selected state or not. This is simply a O(n) lookup done by each row to either show or not show selected state.

Which is solved by either objects or Maps which is done in O(1)

Last but not the least, its just wonderful to have conversation on a topic, since i am trying to constantly learn myself, and learn from my mistakes. Its was a great conversation @lukeshiru , i learnt a lot from our conversation.

Cheers!

Collapse
assertnotnull profile image
Patrice Gauthier

For omitting a property of an object there is ramdajs.com/docs/#dissoc
and ramdajs.com/docs/#dissocPath for nested properties.
Once you get familiar with this lib you see the potential it has

Collapse
js_bits_bill profile image
JS Bits with Bill

@lukeshiru
Wouldn't map.delete(key) be considered a mutation? Why is delete obj.key an evil mutation but not deleting from a map?

Collapse
lukeshiru profile image
LUKESHIRU

I never said deleting on a Map is not evil, on the contrary, that's yet another reason I don't like Maps, they are made to be mutated :/

Thread Thread
js_bits_bill profile image
JS Bits with Bill • Edited

Thanks for clarifying. But if delete and map.delete() are both evil mutations, then what's the answer?

Thread Thread
lukeshiru profile image
LUKESHIRU

I shown a few examples, but the short answer would be "copy with omission". You don't delete, you ommit values from the copy. Is a pretty common practice with immutability.

Thread Thread
js_bits_bill profile image
JS Bits with Bill

Gotcha. Is there an argument to be made to accept small levels of mutability with good documentation in order to avoid what could be large amounts of overhead or extra frameworks in practicing 100% immutability? I feel like it may add more complexity than necessary for smaller projects.

Collapse
blackr1234 profile image
blackr1234 • Edited

May I ask some questions which may seem silly:

Q1a: Why are you not providing the value when you call selectedItems.set in your Map practical example?

Q1b: Since you are not providing the value, could you simply use an array or Set of strings in your Map practical example? If you need helper methods, Set does have add and delete.

Q2: When you talk about performances of object deletion vs map entry deletion, you do not provide figures or references. Could you elaborate more on it please? I can't imagine why deleting a key from an object will have performance issue.

Update:

I see your conversation with LUKESHIRU and I think you use Map over an array or Set mainly because of performance ("scalability") as the Map implementation in V8 is a hash map. I think you should include this in your article stressing that performance is a huge consideration (at least in your practical example) because "why not an array or Set" may be the first thing people think of when they read your Map practical example. Otherwise, it is a great article!!

However, it will only be an issue when you have tons of records inside the array or Set of selected items I guess (I did not test it) when you navigate between the rightmost pages or pages without selection and it does lookups in O(N) multiple times. In this case, without using a Map, I think we can have an array of array/Set of strings, where the first layer of array is the page number and the second layer is the selected items on that page. I think it should perform pretty well, but to obtain a flat list of strings, we will need to flat map the array.

Collapse
faisalpathan profile image
faisal khan Author

Thanks @blackr1234 , yup i will try and include the performance specific point as well.

Whenever i have to render items and want to do a lookup to a variable to get an idea about what states need to render, i usually start with using array's and then gradually move to either objects or maps depending upon the use-case(this always happens post profiling session).

Collapse
supportic profile image
Supportic

Cannot read maps as config files like json or yaml files.

Collapse
faisalpathan profile image
faisal khan Author

Good point thanks

Collapse
darrylnoakes profile image
Darryl Noakes

You can iterate over the keys of an object using for ... in. Same as you can ierate over the indices (i.e. keys) of an array.

Collapse
faisalpathan profile image
faisal khan Author

Thanks for commenting, i agree, but i don't prefer using for ... in, since i have to also have a check in place using .hasOwnProperty to find only the properties and methods you assigned to the object.

Collapse
danwalsh profile image
Dan Walsh

Super interesting stuff. With Redux strongly advising against using Maps in state, are there any options to work around this?

Collapse
lukeshiru profile image
LUKESHIRU

Redux has that advice because they work with immutability, and Maps are designed to be mutated.

Collapse
faisalpathan profile image
faisal khan Author

We can create a custom middleware in redux to serialize maps, but the conditon is the key has to serializable.

Collapse
sebbdk profile image
Sebastian Vargr • Edited

In 15 years, I never strictly needed to use a map type in JS, or found object being a performance bottleneck.

I can imagine a few edge cases where it would make sense, but apart from that KISS. :)

Collapse
sativ01 profile image
sativ01

In my 5y career I'm using Maps at least couple of times per year.
I replace arrays that are often searched through using Array.find()
Check your projects, maybe there's a use case too

Collapse
sebbdk profile image
Sebastian Vargr

hmm maybe, i think the most use-cases for this i have are usually state related, and since i want state to be serialisable into json, i would have to make conversion to/from maps to get the benefit. :/

Do you have any none state related examples i can use to think about this?

Collapse
aspiiire profile image
Aspiiire

Well, this is a Great article, thanks for sharing :D

Collapse
sativ01 profile image
sativ01

One major thing not mentioned here is that Maps are ordered.
If order matters, then there's not much choice.

Some comments have been hidden by the post's author - find out more