When to use Maps instead of plain JavaScript Objects ?
The plain JavaScript Object { key: 'value' } holds structured data. But plain JS object has its limitations :
Only strings and symbols can be used as keys of Objects. If we use any other things say, numbers as keys of an object then during accessing those keys we will see those keys will be converted into strings implicitly causing us to lose consistency of types. const names= {1: 'one', 2: 'two'}; Object.keys(names); // ['1', '2']
There are chances of accidentally overwriting inherited properties from prototypes by writing JS identifiers as key names of an object (e.g. toString, constructor etc.)
Another object cannot be used as key of an object, so no extra information can be written for an object by writing that object as key of another object and value of that another object will contain the extra information
Objects are not iterators
The size of an object cannot be determined directly
These limitations of Objects are solved by Maps but we must consider Maps as complement for Objects instead of replacement. Basically Map is just array of arrays but we must pass that array of arrays to the Map object as argument with new keyword otherwise only for array of arrays the useful properties and methods of Map aren't available. And remember key-value pairs inside the array of arrays or the Map must be separated by commas only, no colons like in plain objects.
3 tips to decide whether to use a Map or an Object :
Use maps over objects when keys are unknown until run time because keys formed by user input or unknowingly can break the code which uses the object if those keys overwrite the inherited properties of the object, so map is safer in those cases. Also use maps when all keys are the same type and all maps are the same type.
Use maps if there is a need to store primitive values as keys.
Use objects if we need to operate on individual elements.
Benefits of using Maps are :
1. Map accepts any key type and preserves the type of key :
We know that if the object's key is not a string or symbol then JS implicitly transforms it into a string. On the contrary, Map accepts any type of keys : string, number, boolean, symbol etc. and Map preserves the original key type. Here we will use number as key inside a Map and it will remain a number :
const numbersMap= new Map();
numbersMap.set(1, 'one');
numbersMap.set(2, 'two');
const keysOfMap= [...numbersMap.keys()];
console.log(keysOfMap); // [1, 2]
Inside a Map we can even use an entire object as a key. There may be times when we want to store some object related data, without attaching this data inside the object itself so that we can work with lean objects but want to store some information about the object. In those cases we need to use Map so that we can make Object as key and related data of the object as value.
const foo= {name: foo};
const bar= {name: bar};
const kindOfMap= [[foo, 'Foo related data'], [bar, 'Bar related data']];
But the downside of this approach is the complexity of accessing the value by key, as we have to loop through the entire array to get the desired value.
function getBy Key(kindOfMap, key) {
for (const [k, v] of kindOfMap) {
if(key === k) {
return v;
}
}
return undefined;
}
getByKey(kindOfMap, foo); // 'Foo related data'
We can solve this problem of not getting direct access to the value by using a proper Map.
const foo= {name: 'foo'};
const bar= {name: 'bar'};
const myMap= new Map();
myMap.set(foo, 'Foo related data');
myMap.set(bar, 'Bar related data');
console.log(myMap.get(foo)); // 'Foo related data'
We could have done this using WeakMap, just have to write, const myMap= new WeakMap( ). The differences between Map and WeakMap are that WeakMap allows for garbage collection of keys (here objects) so it prevents memory leaks, WeakMap accepts only objects as keys, and WeakMap has reduced set of methods.
2. Map has no restriction over key names :
For plain JS objects we can accidentally overwrite property inherited from the prototype and it can be dangerous. Here we will overwrite the toString( ) property of the actor object :
const actor= {
name: 'Harrison Ford',
toString: 'Actor: Harrison Ford'
};
Now let's define a fn isPlainObject( ) to determine if the supplied argument is a plain object and this fn uses toString( ) method to check it :
function isPlainObject(value) {
return value.toString() === '[object Object]';
}
isPlainObject(actor); // TypeError : value.toString is not a function
// this is because inside actor object toString property is a string instead of inherited method from prototype
The Map does not have any restrictions on the key names, we can use key names like toString, constructor etc. Here although actorMap object has a property named toString but the method toString( ) inherited from prototype of actorMap object works perfectly.
const actorMap= new Map();
actorMap.set('name', 'Harrison Ford');
actorMap.set('toString', 'Actor: Harrison Ford');
function isMap(value) {
return value.toString() === '[object Map]';
}
console.log(isMap(actorMap)); // true
If we have a situation where user input creates keys then we must take those keys inside a Map instead of a plain object. This is because user may choose a custom field name like, toString, constructor etc. then such key names in a plain object can potentially break the code that later uses this object. So the right solution is to bind the user interface state to a map, there is no way to break the Map :
const userCustomFieldsMap= new Map([['color', 'blue'], ['size', 'medium'], ['toString', 'A blue box']]);
3. Map is iterable :
To iterate a plain object's properties we need Object.entries( ) or Object.keys( ). The Object.entries(plainObject) returns an array of key value pairs extracted from the object, we can then destructure those keys and values and can get normal keys and values output.
const colorHex= {
'white': '#FFFFFF',
'black': '#000000'
}
for(const [color, hex] of Object.entries(colorHex)) {
console.log(color, hex);
}
//
'white' '#FFFFFF'
'black' '#000000'
As Maps are iterable that's why we do not need entries( ) methods to iterate over a Map and destructuring of key, value array can be done directly on the Map as inside a Map each element lives as an array of key value pairs separated by commas.
const colorHexMap= new Map();
colorHexMap.set('white', '#FFFFFF');
colorHexMap.set('black', '#000000');
for(const [color, hex] of colorHexMap) {
console.log(color, hex);
}
//'white' '#FFFFFF' 'black' '#000000'
Also map.keys( ) returns an iterator over keys and map.values( ) returns an iterator over values.
4. We can easily know the size of a Map
We cannot directly determine the number of properties in a plain object. We need a helper fn like, Object.keys( ) which returns an array with keys of the object then using length property we can get the number of keys or the size of the plain object.
const exams= {'John Rambo': '80%', 'James Bond': '60%'};
const sizeOfObj= Object.keys(exams).length;
console.log(sizeOfObj); // 2
But in the case of Maps we can have direct access to the size of the Map using map.size property.
const examsMap= new Map([['John Rambo', '80%'], ['James Bond', '60%']]);
console.log(examsMap.size);