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

PS. See Destructuring nested arrays and objects.

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.