Maps
Collection of unique key-value pairs
It may not always be clear why maps (and sets, discussed in the next chapter) exist in JavaScript while regular objects are available and can seemingly do the same. But maps have a few advantages over regular objects that make maps preferable in some cases. In the section Objects vs. maps objects and maps will be compared.
In JavaScript maps are not of an explicit data type. JavaScript provides a predefined Map
object.
Maps are collections of key-value pairs. Both key and value can be of any data type, where in regular objects keys can only be either strings or symbols.
A key must be unique in the map's collection.
The number of key-value pairs can easily be obtained by the size
property, somewhat like the length
property of an array.
Maps are iterables, but they are not array-like.
They have a Symbol.iterator
method, but do not have indexed properties and a length
property.
const myMap = new Map();
myMap.set('a', 1);
myMap.set(true, 2);
myMap.set(313, 3);
console.log(myMap); // logs: { a → 1, true → 2, 313 → 3 }
console.log(myMap.size); // logs: 3
console.log(typeof myMap); // logs: "object"
console.log(Object.getPrototypeOf(myMap)); // logs: Map.prototype
myMap.size = 0;
console.log(myMap.size); // logs: 3
myMap.clear();
console.log(myMap.size); // logs: 0
Each key must be unique in the map's collection.
However, multiple keys in a map cannot be NaN
, even though NaN !== NaN
and
one key can be 0
while another is -0
, even though 0 === -0
.
In the next example both keys reference a different object, so they are considered different keys.
const myMap = new Map();
myMap.set({a: 1}, 1);
myMap.set({a: 1}, 2);
console.log(myMap); // logs: { { a: 1 } → 1, { a: 1 } → 2 }
Constructor
Maps cannot be created with a literal. They are created with the Map
constructor and the new
operator.
Key-value pairs are inserted into the map by the set()
method.
const myMap = new Map();
const key1 = 'Hello';
const key2 = {};
const key3 = function() {};
const key4 = null;
myMap.set(key1, "you!");
myMap.set(key2, {});
myMap.set(key3, true);
myMap.set(key4, undefined);
console.log(myMap); // logs: { Hello → "you!", {} → {}, key3() → true, null → undefined }
The Map
constructor takes an optional argument. If this argument is an iterable object whose elements are key-value pairs,
then each key-value pair is added to the new map. The argument could for instance be an array with elements being arrays of two elements.
This can be used to transform a 2D key-value array into a map, copy maps or merge maps (see later).
const myArray = [[1, true], ["hello", {}], [Symbol("foo"), null]];
const myMap = new Map(myArray);
console.log(myMap); // logs: { 1 → true, hello → {}, Symbol("foo") → null }
It also provides a way to create and fill a map in a little more compact way than using the Map
constructor
and the set
method.
const myMap = new Map([
[1, true],
["hello", {}],
[Symbol("foo"), null]
]);
console.log(myMap); // logs: { 1 → true, hello → {}, Symbol("foo") → null }
Accessing and iterating over key-value pairs
The map's key-value pairs are not really object properties, like the elements of an array are.
As mentioned, the keys can be of any data type and they are not accessible via the regular dot notation or square bracket notation.
Enumerating properties, for instance by means of a for...in
loop, will not visit the key-value pairs, while it will
visit elements in an array.
Key-value pair's values are accessible by the get
method and the key as the argument.
This does not work if the key is an object, since the argument references a different object than the key ({} !== {}
).
const myMap = new Map([
[true, "one" ],
[{ a:2 }, "two"],
[[ 2 ], "three"]
]);
console.log(myMap.get(true)); // logs: "one"
console.log(myMap.get({ a:2 })); // logs: undefined
console.log(myMap.get([ 2 ])); // logs: undefined
Key-value pairs can be iterated over in insertion order, i.e., the order in which each key-value pair was first inserted into the map.
In a for...of
loop the map's built-in iterator
returns an array [key, value]
for every key-value pair.
The Map.prototype.forEach()
method
works alike Array.prototype.forEach()
, except Map.prototype.forEach()
iterates insertion order.
const myMap = new Map();
myMap.set('a', 1);
myMap.set(true, 2);
myMap.set(313, 3);
console.log(myMap['a']); // logs: undefined
console.log(Object.getOwnPropertyNames(myMap)); // logs: []
console.log(myMap.get('a')); // logs: 1
myMap.prop = "someValue";
console.log(myMap.prop); // logs: "someValue"
console.log(myMap.size); // logs: 3
for (let propertyName in myMap) {
console.log(propertyName + ": " + myMap[propertyName]);
}
// logs: "prop: someValue"
const pairs = [];
for (const pair of myMap) {
pairs.push(pair);
}
console.log(pairs); // logs: [ [ "a", 1 ], [ true, 2 ], [ 313, 3 ] ]
// Array destructuring:
for (const [key, value] of myMap) {
console.log(`key: ${key}, value: ${value}.`);
}
// logs:
// key: a, value: 1.
// key: true, value: 2.
// key: 313, value: 3.
myMap.forEach((value,key) => console.log(`${key}, ${value}`));
// logs:
// "a, 1"
// "true, 2"
// "313, 3"
Method keys()
returns an iterator/iterable that yields the key for each element in the map in insertion order.
Method values()()
returns an iterator/iterable that yields the value for each element in the map in insertion order.
Method has()
checks if a key-value pair exist in the map by key.
const myMap = new Map();
myMap.set('Hello!', {});
myMap.set(true, false);
myMap.set(null, 3);
console.log(myMap.has("Hello!")); //logs: true
const keys = myMap.keys();
for (const key of keys) {
console.log(`${key}, ${myMap.get(key)}`);
}
// logs:
// "Hello!, [object Object]"
// "true, false"
// "null, 3"
const values = myMap.values();
for (const value of values) {
console.log(value);
}
// logs:
// {}
// false
// 3
for (const value of values) {
console.log(value);
}
// logs nothing; an object that is both an iterator and an iterable
// can only be iterated over once.
BTW. An iterable iterator with Symbol.iterator
returning the iterable itself, can
only be iterated over once.
Deleting key-value pairs
The Map.prototype.delete()
method can be used to delete a key-value pair from a map by key.
The size
property cannot be used to truncate a map, unlike the length
property on an array.
Method clear
can be used to delete all key-value pairs.
const myMap = new Map();
myMap.set('Hello!', 1);
myMap.set(true, 2);
myMap.set(null, 3);
myMap.delete(true);
console.log(myMap); // logs: { "Hello!" → 1, null → 3 }
console.log(myMap.has(true)); //logs: false
myMap.size = 0;
console.log(myMap.size); // logs: 2
myMap.clear();
console.log(myMap.size); // logs: 0
Cloning and merging maps
We have seen that we can use the Map
constructor taking an iterable object as the argument, whose elements are key-value pairs.
We can use this to clone or merge maps. The clones and merges are shallow, meaning the data itself is not cloned.
You can directly use the original map as argument, but you can also use spread syntax in an array as the argument. In the latter case, maps essentially convert to arrays first. This may be a little unwieldy for just cloning a map, but useful to merge maps, as will be explained next.
const originalMap = new Map([
[1, 'one'],
[2, 'two'],
]);
const cloneMap = new Map(originalMap);
console.log(cloneMap); // logs: { 1 → "one", 2 → "two" }
console.log(originalMap === cloneMap); // false
const otherCloneMap = new Map([...originalMap]);
console.log(otherCloneMap); // logs: { 1 → "one", 2 → "two" }
Maps can be merged using an array argument for the constructor. This way you can merge any iterable objects (with key-value pairs) to a map. The key uniqueness will be maintained by overwriting a previous key-value pair with the last iterated over pair with a key with the same name.
const firstMap = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
const secondMap = new Map([
[1, 'unus'],
[2, 'duo'],
]);
const mergedMap = new Map([...firstMap, ...secondMap]);
console.log(mergedMap); // logs: { 1 → "unus", 2 → "duo", 3 → "three" }
mergedMap.set(4, "fyra");
const myArray = [[2, "två"], [3, "tre"]];
const composedMap = new Map([...mergedMap, [1, 'ett'], ...myArray]);
console.log(composedMap); // logs: { 1 → "ett", 2 → "två", 3 → "tre", 4 → "fyra" }
Converting to and from maps
We have seen that we can use the Map
constructor to convert iterable objects, whose elements are key-value pairs,
to maps.
const myArray = [[9007199254740991n, true], [null, {}], [Symbol("foo"), null]];
const myMap = new Map(myArray);
console.log(myMap);
// logs: { 9007199254740991n → true, null → {}, Symbol("foo") → null }
We can convert maps into arrays by using Array.from()
or spread syntax.
Note that all conversions are shallow.
const myMap = new Map([[9007199254740991n, true], [null, {a: 1}], [Symbol("foo"), null]]);
console.log(Array.from(myMap));
// logs: [[9007199254740991n, true], [null, {a: 1}], [Symbol("foo"), null]]
console.log(Array.from(myMap.keys()));
// logs: [ 9007199254740991n, null, Symbol("foo") ]
console.log([...myMap]);
// logs: [[9007199254740991n, true], [null, {a: 1}], [Symbol("foo"), null]]
// The conversions are shallow:
const convertedMapOne = [...myMap];
const convertedMapTwo = Array.from(myMap);
console.log(convertedMapOne === convertedMapTwo); // logs: false (only shallow comparison)
convertedMapOne[1][1]['a'] = 2;
console.log(convertedMapTwo[1][1]['a']); // logs: 2
console.log(convertedMapOne[1][1]['a']); // logs: 2
console.log(convertedMapOne[1][1] === convertedMapTwo[1][1]); // logs: true // on a deeper level they both share the same object
Conversion from a regular object to a map can be done via the
Object.entries()
method
which returns an array of the object's own enumerable string-keyed property [key, value]
pairs.
const myObject = {a: 1, b: 2}
const myMap = new Map(Object.entries(myObject));
console.log(myMap); // logs: { a → 1, b → 2 }
Conversion from a map to a regular object can be done via the
Object.fromEntries()
method
which transforms a list of key-value pairs into an object.
Map keys that are not strings or symbols are converted to strings.
const myMap = new Map([
['a', 1],
['b', 2]
]);
myMap.set(true, false);
const myObject = Object.fromEntries(myMap);
console.log(myObject); // logs: { a: 1, b: 2, true: false } // "true" is a string here!
Destructuring a map
Array destructuring calls the iterable protocol of the right-hand side. Therefore, any iterable, including a map, can be destructured.
const myMap = new Map([
['a', 1],
['b', 2],
['c', 3],
['d', 4]
]);
const [a, b] = myMap;
console.log(a, b); // logs: [ "a", 1 ] [ "b", 2 ]
//Destructuring nested array:
const [[,c], [,d]] = myMap;
console.log(c, d); // logs: 1 2
Objects vs. maps
Objects also map strings (or symbols) to values, being key-value pairs. Why not always use regular objects? Maps have a few advantages that, in some cases, make them more suitable key-value mappings than regular objects.
- The keys of an object are either strings or symbols. Keys in a map can be of any data type.
-
You can easily get the number of items in a map (using
size
), while you have to manually keep track of the number of properties in an object. - Objects are unordered, meaning that enumerating properties happens in an arbitrary order. Maps always iterate keys, values and key-value pairs in the order of entry insertion.
-
Maps iterate only over key-value pairs that were explicitly put in the collection.
Objects may also enumerate over possible inherited properties or over other properties not meant to be in the collection of mapped key-value pairs.
Pollution with inherited properties can be bypassed by using
Object.create(null)
to create an object with prototypenull
, but these are just cheap substitutes for maps. - Objects are not optimized for frequent additions and removals of key-value pairs. Maps perform better in these scenarios.