TLDR
- Map can have any value as it's keys, objects can only have strings or symbols
- Maps are ordered based on insertion
- Maps are easier to clear out than objects
- Destructuring works differently with Maps vs Objects
- WeakMap provides garbage collection benefits over regular Maps
ES6, also known as ECMAScript 2015, introduced many new features. In this article, I'll be discussing a new data structure that was introduced in ES6 known as a Map and the benefits/drawbacks of using a Map over a regular javascript object.
- We're on ES10 now, should I still care about ES6?
- Can't I just keep using regular objects?
The answer to both questions above is yes. Although you can still use a plain object, knowing when and how to use a Map provides many benefits that can make your code much cleaner.
What is a Map?
A Map is structured very similar to an object, in that it holds key/value pairs. The distinction is how and where it stores those values.
If you want to initialize the Map with data, you can pass an array containing or an iterable object with key/value pairs as an argument of the constructor.
const map = new Map(); // Create a empty Map
// Map initialized containing [ 'one', 1 ], [ 'two', 2 ]
const anotherMap = new Map([ [ 'one', 1 ], [ 'two', 2 ] ]);
The two biggest benefits regarding how the data is stored in a Map:
- An object's keys are set as either a String or a Symbol, but a Map can have anything set as the key...including functions, objects and primitives.
- Maps are ordered based on order of insertion
Keys in Maps
Lets dig a little deeper into the differences between keys in objects and a Map:
Since keys get stored as strings, non-strings will get coerced. What this means is that basically "1" and 1 are the same when they get set as keys of an object.
const obj = {};
// Integers will be casted to a string
obj[1] = 'one';
obj['1'] // one
obj[1] // one
// Keys will not be casted when setting keys for a Map
const map = new Map();
map.set(1, 'one'); // a numeric key
map.set('1', 'another one'); // a string key
// map will contain two items: 1, 'one' and '1', 'another one'
When I said anything can be set as a key...I mean anything:
const person = {
name: 'John'
}
const map = new Map();
// Set an object as a key
map.set(person, 30);
map.get(person); // 30
// You can even use a map as a key for a map!
const anotherMap = new Map();
anotherMap.set(map, true);
anotherMap.get(map); // true
Maps use the sameValueZero
algorithm when comparing keys. This is pretty similar to strict equality ===
but also considers NaN === NaN
.
In objects you will need to set key/values one at a time, but since Map.set()
returns the map you can chain calls:
const map = new Map();
map.set(1, 'one')
.set(2, 'two')
.set(3, 'three')
.entries();
// 1 => "one", 2 => "two", 3 => "three"
Deleting properties
Deleting properties from objects and Maps are pretty similar, but Maps provide a few extra benefits.
When deleting a property from an object, it will always return true unless the property is a non-configurable property.
When deleting a property from a Map, it will return true if the property existed and has been removed, otherwise it will return false if it doesn't exist.
// deleting properties from objects
const obj = {
one: 'one'
}
delete obj.one // true
delete obj.two // also true
// deleting properties from Maps
const map = new Map()
map.set('one', 'one')
map.delete('one') // true
map.delete('two') // false
But what if you want to delete all properties that belong to that object?
You could do:
const obj = {
one: 'one'
}
obj = {}
In this implementation you're not really removing properties, you're just setting obj to a new empty object and relying on the garbage collector to clean up the old object. The issue is that if the object is being referenced elsewhere, it will still exist. A better implementation would be:
for (let key in obj){
if (obj.hasOwnProperty(key)){
delete obj[key];
}
}
This is better but still doesn't handle keys that are Symbols.
Maps make it very easy to clear all it's elements regardless what the key is:
const values = [['1', 'one'], [true, '5'], [Symbol('test'), 10], [function() {}, 3]]
const map = new Map(values)
map.clear() // completely empties out the map
According to MDN docs regarding performance:
A Map may perform better in scenarios involving frequent addition and removal of key pairs.
Iterating Maps
As I mentioned in the beginning of this article, unlike objects, Maps are ordered based on insertion which makes iterating more predictable.
const obj = {};
obj[5] = 'five';
obj[4] = 'four';
Object.entries(obj); // [ ['4', 'four'], ['5', "five"] ]
const map = new Map();
map.set(5, 'five')
.set(4, 'four')
.entries(); // [ 5 => "five", 4 => "four" ]
Similar to objects, there are three methods you can use for looping over Maps:
-
map.keys()
returns an iterable containing the keys -
map.values()
returns an iterable containing the values -
map.entries()
returns an iterable containing the[key, value]
pairs
Objects use Object.keys
, Object.values
, and Object.entries
. One main difference is that these return arrays whereas the map methods return iterables.
const obj = {
one: 1,
two: 2,
};
for (let key of Object.keys(obj)) {
console.log(key)
} // logs "one" then "two"
for (let value of Object.values(obj)) {
console.log(value)
} // logs 1 then 2
for (let entry of Object.entries(obj)) {
console.log(entry)
} // logs ["one", 1] then ["two", 2]
Maps work similarly:
const map = new Map([["one", 1], ["two", 2]]);
for (let key of map.keys()) {
console.log(key)
} // logs "one" then "two"
for (let value of map.values()) {
console.log(value)
} // logs 1 then 2
for (let entry of map.entries()) {
console.log(entry)
} // logs ["one", 1] then ["two", 2]
// One difference is that map.entries() is used by default in a for..of loop
for (let entry of map) {
console.log(entry)
} // still logs ["one", 1] then ["two", 2]
Note: Since anything can be set as a key, maps will always iterate over all items. But for objects there are some properties that won't be iterated by default like Symbols.
Converting between Maps and objects
Now that you know some of the differences, it might be helpful to know how to convert an object to a Map or vise versa to take advantages of the benefits of each data structure.
A Map requires an array or iterable, so we can use Object.entries
to get the key/value pairs as an array and pass it into the constructor:
const obj = {
'one': 1,
'two': 2,
}
const map = new Map(Object.entries(obj));
console.log(map.get('one')) // 1
Ok, that looks simple enough...but how the heck to we create an object from a Map? Luckily we have Object.fromEntries
which basically works the reverse way of Object.entries
:
const map = new Map();
map.set('one', 1);
map.set('two', 2);
const obj = Object.fromEntries(map.entries());
const obj = Object.fromEntries(map); // Or we can even omit the entries() since that's used by default
console.log(obj.one') // 1
Destructuring
Since Maps are ordered similar to arrays, you lose the ability to destructure by keys like you can do with objects.
const obj = {
one: 1,
two: 2,
three: 3,
}
let { one, two } = obj;
console.log(one) // 1
Now lets try destructuring a Map:
const map = new Map([ [ 'one', 1], ['two', 2] ]);
let { one, two } = map;
console.log(one) // undefined
// But you can destructure it similar to an array where you destructure by the order items were added into the map
let [ firstEntry, secondEntry ] = map;
console.log(firstEntry) // ["one", 1]
console.log(secondEntry) // ["two", 2]
Map vs WeakMap
Now that you're a Map connoisseur, it will be beneficial to learn a little about WeakMap which was also introduced in ES6.
One main difference when using a WeakMap is that the keys have to be objects, not primitive values. Which means they will pass by reference.
So why use a WeakMap? The major advantage of using a WeakMap over a Map is memory benefits.
Objects that are not-reachable get garbage collected, but if they exist in as a key in another reachable structure then they won't get garbage collected. Lets look at an example:
let obj = { name: 'Matt' } // object can be accessed
let obj = null // overwrite the reference, the object above will be garbage collected and removed from memory
If the object is still reachable it will not be removed from memory:
let obj = { name: 'Matt' } // object can be accessed
let map = new Map();
map.set(obj, true);
obj = null // overwrite the reference, but since it's still reachable through the map, the object will not be garbage collected
WeakSet does not prevent garbage-collection of it's key objects.
let obj = { name: 'Matt' } // object can be accessed
let weakMap = new WeakMap();
weakMap.set(obj, true);
obj = null // overwrite the reference, the object was removed from memory
// weakMap is now empty
WeakMaps only have the following methods: get
, set
, delete
, has
.
Why just those? Because the Javascript engine handles the memory cleanup so it may choose to clean it up immediately or wait until more deletions happen.
Therefore things like the current count of a WeakMap will never fully be accurate.
So when would you ever use a WeakMap?
When you want to use it for additional storage that only lasts until the reference is destroyed.
Lets say as an example you have users and you want to increase the count anytime they visit a page, but you no longer care once the user is gone.
let userCountStorage = new WeakMap();
let user = { name: 'matt' };
incrementCount(user); // pretend this function adds the user to the userCountStorage or increments the count if they already exists
// increment every time they visit a page
incrementCount(user);
incrementCount(user);
// Now they're gone so we get rid of the reference
user = null // since the object is no longer reachable, the garbage collector will automatically also remove the item from our userCountStorage
In the above example if we used Map instead of WeakMap we would run into memory issues if we didnt manually remove the references from the storage once we destroyed the reference elsewhere.
Summary
Using a Map or object is always situational but hopefully now you've learned some benefits and drawbacks for using each data structure.
More on Maps/Objects:
Top comments (3)
I love the detail you went into here! Great post. Minor nits: I'd make the TDLR the first thing in the post, and I'd add WeakMap to the title since you spent time discussing it.
Thanks for the feedback! First article I've ever written, so hopefully it's not too unorganized. I also wasn't originally going to talk about WeakMap but threw that in there at the end, but you're totally right so I updated the title & moved the TLDR.
Great first post!! I started recently as well