Please support this book: buy it or donate

30. Maps (Map)



Before ES6, JavaScript didn’t have a data structure for dictionaries and (ab)used objects as dictionaries from strings to arbitrary values. ES6 brought Maps, which are dictionaries from arbitrary values to arbitrary values.

30.1. Using Maps

An instance of Map maps keys to values. A single key-value mapping is called an entry. Maps record in which order entries were created and honor that order when returning, e.g., keys or entries.

30.1.1. Creating Maps

There are three common ways of creating Maps.

First, you can use the constructor without any parameters to create an empty Map:

  1. const emptyMap = new Map();
  2. assert.equal(emptyMap.size, 0);

Second, you can pass an iterable (e.g. an Array) over key-value “pairs” (Arrays with 2 elements) to the constructor:

  1. const map = new Map([
  2. [ 1, 'one' ],
  3. [ 2, 'two' ],
  4. [ 3, 'three' ], // trailing comma is ignored
  5. ]);

Third, the .set() method adds entries to a Map and is chainable:

  1. const map = new Map()
  2. .set(1, 'one')
  3. .set(2, 'two')
  4. .set(3, 'three');

30.1.2. Working with single entries

.set() and .get() are for writing and reading values (given keys).

  1. const map = new Map();
  2. map.set('foo', 123);
  3. assert.equal(map.get('foo'), 123);
  4. // Unknown key:
  5. assert.equal(map.get('bar'), undefined);
  6. // Use the default value '' if an entry is missing:
  7. assert.equal(map.get('bar') || '', '');

.has() checks if a Map has an entry with a given key. .delete() removes entries.

  1. const map = new Map([['foo', 123]]);
  2. assert.equal(map.has('foo'), true);
  3. assert.equal(map.delete('foo'), true)
  4. assert.equal(map.has('foo'), false)

30.1.3. Determining the size of a Map and clearing it

.size contains the number of entries in a Map. .clear() removes all entries of a Map.

  1. const map = new Map()
  2. .set('foo', true)
  3. .set('bar', false)
  4. ;
  5. assert.equal(map.size, 2)
  6. map.clear();
  7. assert.equal(map.size, 0)

30.1.4. Getting the keys and values of a Map

.keys() returns an iterable over the keys of a Map:

  1. const map = new Map()
  2. .set(false, 'no')
  3. .set(true, 'yes')
  4. ;
  5. for (const key of map.keys()) {
  6. console.log(key);
  7. }
  8. // Output:
  9. // false
  10. // true

We can use spreading () to convert the iterable returned by .keys() to an Array:

  1. assert.deepEqual(
  2. [...map.keys()],
  3. [false, true]);

.values() works like .keys(), but for values instead of keys.

30.1.5. Getting the entries of a Map

.entries() returns an iterable over the entries of a Map:

  1. const map = new Map()
  2. .set(false, 'no')
  3. .set(true, 'yes')
  4. ;
  5. for (const entry of map.entries()) {
  6. console.log(entry);
  7. }
  8. // Output:
  9. // [false, 'no']
  10. // [true, 'yes']

Spreading () converts the iterable returned by .entries() to an Array:

  1. assert.deepEqual(
  2. [...map.entries()],
  3. [[false, 'no'], [true, 'yes']]);

Map instances are also iterables over entries. In the following code, we use destructuring to access the keys and values of map:

  1. for (const [key, value] of map) {
  2. console.log(key, value);
  3. }
  4. // Output:
  5. // false, 'no'
  6. // true, 'yes'

30.2. Example: Counting characters

countChars() returns a Map that maps characters to numbers of occurrences.

  1. function countChars(chars) {
  2. const charCounts = new Map();
  3. for (let ch of chars) {
  4. ch = ch.toLowerCase();
  5. const prevCount = charCounts.get(ch) || 0;
  6. charCounts.set(ch, prevCount+1);
  7. }
  8. return charCounts;
  9. }
  10. const result = countChars('AaBccc');
  11. assert.deepEqual(
  12. [...result],
  13. [
  14. ['a', 2],
  15. ['b', 1],
  16. ['c', 3],
  17. ]
  18. );

30.3. A few more details about the keys of Maps (advanced)

Any value can be a key, even an object:

  1. const map = new Map();
  2. const KEY1 = {};
  3. const KEY2 = {};
  4. map.set(KEY1, 'hello');
  5. map.set(KEY2, 'world');
  6. assert.equal(map.get(KEY1), 'hello');
  7. assert.equal(map.get(KEY2), 'world');

30.3.1. What keys are considered equal?

Most Map operations need to check whether a value is equal to one of the keys. They do so via the internal operation SameValueZero, which works like ===, but considers NaN to be equal to itself.

As a consequence, you can use NaN as a key in Maps, just like any other value:

  1. > const map = new Map();
  2. > map.set(NaN, 123);
  3. > map.get(NaN)
  4. 123

Different objects are always considered to be different. That is something that can’t be configured (yet – TC39 is aware that this is important functionality).

  1. > new Map().set({}, 1).set({}, 2).size
  2. 2

30.4. Missing Map operations

30.4.1. Mapping and filtering Maps

You can .map() and .filter() Arrays, but there are no such operations for Maps. The solution is:

  • Convert the Map into an Array of [key,value] pairs.
  • Map or filter the Array.
  • Convert the result back to a Map.
    I’ll use the following Map to demonstrate how that works.
  1. const originalMap = new Map()
  2. .set(1, 'a')
  3. .set(2, 'b')
  4. .set(3, 'c');

Mapping originalMap:

  1. const mappedMap = new Map( // step 3
  2. [...originalMap] // step 1
  3. .map(([k, v]) => [k * 2, '_' + v]) // step 2
  4. );
  5. assert.deepEqual([...mappedMap],
  6. [[2,'_a'], [4,'_b'], [6,'_c']]);

Filtering originalMap:

  1. const filteredMap = new Map( // step 3
  2. [...originalMap] // step 1
  3. .filter(([k, v]) => k < 3) // step 2
  4. );
  5. assert.deepEqual([...filteredMap],
  6. [[1,'a'], [2,'b']]);

Step 1 is performed by the spread operator ().

30.4.2. Combining Maps

There are no methods for combining Maps, which is why the approach from the previous section must be used to do so.

Let’s combine the following two Maps:

  1. const map1 = new Map()
  2. .set(1, '1a')
  3. .set(2, '1b')
  4. .set(3, '1c')
  5. ;
  6. const map2 = new Map()
  7. .set(2, '2b')
  8. .set(3, '2c')
  9. .set(4, '2d')
  10. ;

To combine map1 and map2, we turn them into Arrays via the spread operator () and concatenate those Arrays. Afterwards, we convert the result back to a Map. All of that is done in line A.

  1. const combinedMap = new Map([...map1, ...map2]); // (A)
  2. assert.deepEqual(
  3. [...combinedMap], // convert to Array for comparison
  4. [ [ 1, '1a' ],
  5. [ 2, '2b' ],
  6. [ 3, '2c' ],
  7. [ 4, '2d' ] ]
  8. );

30.5. Quick reference: Map<K,V>

Note: For the sake of conciseness, I’m pretending that all keys have the same type K and that all values have the same type V.

30.5.1. Constructor

  • new Map<K, V>(entries?: Iterable<[K, V]>) [ES6]

If you don’t provide the parameter entries then an empty Map is created. If you do provide an iterable over [key, value] pairs then those pairs are used to add entries to the Map. For example:

  1. const map = new Map([
  2. [ 1, 'one' ],
  3. [ 2, 'two' ],
  4. [ 3, 'three' ], // trailing comma is ignored
  5. ]);

30.5.2. Map<K,V>.prototype: handling single entries

  • .get(key: K): V [ES6]

Returns the value that key is mapped to in this Map. If there is no key key in this Map, undefined is returned.

  1. const map = new Map([[1, 'one'], [2, 'two']]);
  2. assert.equal(map.get(1), 'one');
  3. assert.equal(map.get(5), undefined);
  • .set(key: K, value: V): this [ES6]

Maps the given key to the given value. If there is already an entry whose key is key, it is updated. Otherwise, a new entry is created. This method returns this, which means that you can chain it.

  1. const map = new Map([[1, 'one'], [2, 'two']]);
  2. map.set(1, 'ONE!');
  3. map.set(3, 'THREE!');
  4. assert.deepEqual(
  5. [...map.entries()],
  6. [[1, 'ONE!'], [2, 'two'], [3, 'THREE!']]);
  • .has(key: K): boolean [ES6]

Returns whether the given key exists in this Map.

  1. const map = new Map([[1, 'one'], [2, 'two']]);
  2. assert.equal(map.has(1), true); // key exists
  3. assert.equal(map.has(5), false); // key does not exist
  • .delete(key: K): boolean [ES6]

If there is an entry whose key is key, it is removed and true is returned. Otherwise, nothing happens and false is returned.

  1. const map = new Map([[1, 'one'], [2, 'two']]);
  2. assert.equal(map.delete(1), true);
  3. assert.equal(map.delete(5), false); // nothing happens
  4. assert.deepEqual(
  5. [...map.entries()],
  6. [[2, 'two']]);

30.5.3. Map<K,V>.prototype: handling all entries

  • get .size: number [ES6]

Returns how many entries there are in this Map.

  1. const map = new Map([[1, 'one'], [2, 'two']]);
  2. assert.equal(map.size, 2);

In JavaScript indexable sequences (such as Arrays) have a .length, while mainly unordered collections (such as Maps) have a .size.

  • .clear(): void [ES6]

Removes all entries from this Map.

  1. const map = new Map([[1, 'one'], [2, 'two']]);
  2. assert.equal(map.size, 2);
  3. map.clear();
  4. assert.equal(map.size, 0);

30.5.4. Map<K,V>.prototype: iterating and looping

Both iterating and looping happen in the order in which entries were added to a Map.

  • .entries(): Iterable<[K,V]> [ES6]

Returns an iterable with one [key,value] pair for each entry in this Map. The pairs are Arrays of length 2.

  1. const map = new Map([[1, 'one'], [2, 'two']]);
  2. for (const entry of map.entries()) {
  3. console.log(entry);
  4. }
  5. // Output:
  6. // [1, 'one']
  7. // [2, 'two']
  • .forEach(callback: (value: V, key: K, theMap: Map<K,V>) => void, thisArg?: any): void [ES6]

The first parameter is a callback that is invoked once for each entry in this Map. If thisArg is provided, this is set to it for each invocation. Otherwise, this is set to undefined.

  1. const map = new Map([[1, 'one'], [2, 'two']]);
  2. map.forEach((value, key) => console.log(value, key));
  3. // Output:
  4. // 'one', 1
  5. // 'two', 2
  • .keys(): Iterable<K> [ES6]

Returns an iterable over all keys in this Map.

  1. const map = new Map([[1, 'one'], [2, 'two']]);
  2. for (const key of map.keys()) {
  3. console.log(key);
  4. }
  5. // Output:
  6. // 1
  7. // 2
  • .values(): Iterable<V> [ES6]

Returns an iterable over all values in this Map.

  1. const map = new Map([[1, 'one'], [2, 'two']]);
  2. for (const value of map.values()) {
  3. console.log(value);
  4. }
  5. // Output:
  6. // 'one'
  7. // 'two'

The default way of iterating over Maps. Same as .entries().

  1. const map = new Map([[1, 'one'], [2, 'two']]);
  2. for (const [key, value] of map) {
  3. console.log(key, value);
  4. }
  5. // Output:
  6. // 1, 'one'
  7. // 2, 'two'

30.5.5. Sources

30.6. FAQ

30.6.1. When should I use a Map, when an object?

If you map anything other than strings to any kind of data, you have no choice: you must use a Map.

If, however, you are mapping strings to arbitrary data, you must decide whether or not to use an object. A rough general guideline is:

  • Is there a fixed set of keys (known at development time)?Then use an object and access the values via fixed keys: obj.key

  • Can the set of keys change at runtime?Then use a Map and access the values via keys stored in variables: map.get(theKey)

30.6.2. When would I use an object as a key in a Map?

Map keys mainly make sense if they are compared by value (the same “content” means that two values are considered equal, not the same identity). That excludes objects. There is one use case – externally attaching data to objects, but that use case is better served by WeakMaps where entries don’t prevent garbage collection (for details, consult the next chapter).