In JavaScript, a Map is an object that stores key/value pairs. Maps are similar to general JavaScript objects, but there are a few key differences between objects and maps that make maps useful.
Maps vs Objects
Suppose you want to create a JavaScript object that stores some key/value paths. You could define a plain-old JavaScript object, or a "POJO" for short, as shown below.
const obj = {
name: 'Jean-Luc Picard',
age: 59,
rank: 'Captain'
};
obj.name; // 'Jean-Luc Picard'
You could also define a map that contains the same keys and values as shown below.
const map = new Map([
// You define a map via an array of 2-element arrays. The first
// element of each nested array is the key, and the 2nd is the value
['name', 'Jean-Luc Picard'],
['age', 59],
['rank', 'Captain']
]);
// To get the value associated with a given `key` in a map, you
// need to call `map.get(key)`. Using `map.key` will **not** work.
map.get('name'); // 'Jean-Luc Picard'
Suppose you wanted to get Captain Picard's age
. With an object,
you can use obj.age
. With a map, you would use map.get('age')
.
That works fine for properties that don't conflict with built-in
JavaScript functionality, but what about if you wanted to get the
object's constructor
property? In this case, obj.constructor
is defined, but map.get('constructor')
is not.
obj.constructor; // [Function: Object]
map.get('constructor'); // undefined
With JavaScript objects, you could use
Object.create(null)
to create an object
that doesn't inherit from any class, and
so doesn't have a constructor
property.
That is one approach to the issue of separating
user data from program data: maps are another.
Maps don't have any notion of inheritance: a map does not have any inherited keys. This makes maps ideal for storing raw data without worrying about that data conflicting with existing methods and properties. For example, maps are immune to prototype pollution, a security vulnerability where naive copying of user data can allow a malicious user to overwrite class methods.
Another key difference is that maps allow you to store object keys, not just strings. This can cause some confusion when you're storing objects like Dates or Numbers as keys.
const map = new Map([]);
const n1 = new Number(5);
const n2 = new Number(5);
map.set(n1, 'One');
map.set(n2, 'Two');
// `n1` and `n2` are objects, so `n1 !== n2`. That means the map has
// separate keys for `n1` and `n2`.
map.get(n1); // 'One'
map.get(n2); // 'Two'
map.get(5); // undefined
// If you were to do this with an object, `n2` would overwrite `n1`
const obj = {};
obj[n1] = 'One';
obj[n2] = 'Two';
obj[n1]; // 'Two'
obj[5]; // 'Two'
Maps also have a size
property that returns the number of key/value
pairs in the map. To get the number of keys in an object, you would
have to do Object.keys(obj).length
.
map.size; // 3
Another difference you'll likely read about is the order of the keys
in a map is guaranteed. In other words, if you call map.keys()
, you
will always get the keys in the order in which they were added to
the map. In the Captain Picard example, map.keys()
will always
return name
, age
, and rank
in that order.
Object key order is also guaranteed for ES6-compliant browsers. For example, Object.keys(obj)
will always return
['name', 'age', 'rank']
in ES6-compliant JavaScript runtimes. But,
in older runtimes (Internet Explorer, etc.) Object.keys(obj)
may
return the keys in a different order.
Map#keys()
, Map#values()
, Map#entries()
Maps have 3 built-in methods for iterating through the map: keys()
, values()
, and entries()
. Unlike Object.keys()
, the Map#keys()
function returns an iterator, not an array. That means the easiest way to
iterate a map's keys is using a for/of
loop.
const map = new Map([
['name', 'Jean-Luc Picard'],
['age', 59],
['rank', 'Captain']
]);
const iterator = map.keys();
console.log(iterator); // MapIterator { 'name', 'age', 'rank' }
// `map.keys()` returns an iterator, not an array, so you can't
// access the values using `[]`
iterator[0]; // undefined
// The `for/of` loop can loop through iterators
for (const key of map.keys()) {
key; // 'name', 'age', 'rank'
}
Sometimes it is handy to convert an iterator into an array so you
can use array methods like filter()
and map()
. The easiest way to convert an iterator into an array is using the built-in Array.from()
function.
const arr = Array.from(map.keys());
arr.length; // 3
arr[0]; // 'name'
arr[1]; // 'age'
arr[2]; // 'rank'
The Map#values()
function also returns an iterator. The iterator
iterates through the map's values:
for (const v of map.values()) {
v; // 'Jean-Luc Picard', 59, 'Captain'
}
Finally, Map#entries()
returns an iterator that iterates through
the map's key/value pairs in a similar format to the Map
constructor. The Map#entries()
function is the Map
class's
equivalent to Object.entries()
.
for (const [key, value] of map.entries()) {
key; // 'name', 'age', 'rank'
value; // 'Jean-Luc Picard', 59, 'Captain'
}
The Map#entries()
function makes it easy to copy a map. Cloning
a map is as simple as converting map.entries()
into an array:
// `clone` is now a separate map that contains the same keys/values
// as `map`.
const clone = new Map(Array.from(map.entries()));
Moving On
Although JavaScript developers typically use objects to store user data, maps have some advantages: there's no risk of prototype pollution or JSON data overwriting class methods. Maps also let you store object keys, which can be useful if you want to associate data with an object without setting a symbol. Maps are still pretty uncommon in open source JavaScript, the only significant use case I've seen is Mongoose's map type. But maps are worth experimenting with for representing JSON data because they avoid the risk of prototype pollution.