Map
)Map<K,V>
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.
An instance of Map
maps keys to values. A single key-value mapping is called an entry.
There are three common ways of creating Maps.
First, you can use the constructor without any parameters to create an empty Map:
Second, you can pass an iterable (e.g. an Array) over key-value “pairs” (Arrays with 2 elements) to the constructor:
Third, the .set()
method adds entries to a Map and is chainable:
.set()
and .get()
are for writing and reading values (given keys).
const map = new Map();
map.set('foo', 123);
assert.equal(map.get('foo'), 123);
// Unknown key:
assert.equal(map.get('bar'), undefined);
// Use the default value '' if an entry is missing:
assert.equal(map.get('bar') || '', '');
.has()
checks if a Map has an entry with a given key. .delete()
removes entries.
const map = new Map([['foo', 123]]);
assert.equal(map.has('foo'), true);
assert.equal(map.delete('foo'), true)
assert.equal(map.has('foo'), false)
.size
contains the number of entries in a Map. .clear()
removes all entries of a Map.
const map = new Map()
.set('foo', true)
.set('bar', false)
;
assert.equal(map.size, 2)
map.clear();
assert.equal(map.size, 0)
.keys()
returns an iterable over the keys of a Map:
const map = new Map()
.set(false, 'no')
.set(true, 'yes')
;
for (const key of map.keys()) {
console.log(key);
}
// Output:
// false
// true
We can use spreading (...
) to convert the iterable returned by .keys()
to an Array:
.values()
works like .keys()
, but for values instead of keys.
.entries()
returns an iterable over the entries of a Map:
const map = new Map()
.set(false, 'no')
.set(true, 'yes')
;
for (const entry of map.entries()) {
console.log(entry);
}
// Output:
// [false, 'no']
// [true, 'yes']
Spreading (...
) converts the iterable returned by .entries()
to an Array:
Map instances are also iterables over entries. In the following code, we use destructuring to access the keys and values of map
:
for (const [key, value] of map) {
console.log(key, value);
}
// Output:
// false, 'no'
// true, 'yes'
Maps record in which order entries were created and honor that order when listing entries, keys or values:
const map1 = new Map([
['a', 1],
['b', 2],
]);
assert.deepEqual(
[...map1.keys()], ['a', 'b']);
const map2 = new Map([
['b', 2],
['a', 1],
]);
assert.deepEqual(
[...map2.keys()], ['b', 'a']);
As long as a Map only uses strings and symbols as keys, you can convert it to an object (via Object.entries()
):
const map = new Map([
['a', 1],
['b', 2],
]);
const obj = Object.fromEntries(map);
assert.deepEqual(
obj, {a: 1, b: 2});
You can also convert an object to a Map with string or symbol keys (via Object.fromEntries()
):
const obj = {
a: 1,
b: 2,
};
const map = new Map(Object.entries(obj));
assert.deepEqual(
map, new Map([['a', 1], ['b', 2]]));
countChars()
returns a Map that maps characters to numbers of occurrences.
function countChars(chars) {
const charCounts = new Map();
for (let ch of chars) {
ch = ch.toLowerCase();
const prevCount = charCounts.get(ch) || 0;
charCounts.set(ch, prevCount+1);
}
return charCounts;
}
const result = countChars('AaBccc');
assert.deepEqual(
[...result],
[
['a', 2],
['b', 1],
['c', 3],
]
);
Any value can be a key, even an object:
const map = new Map();
const KEY1 = {};
const KEY2 = {};
map.set(KEY1, 'hello');
map.set(KEY2, 'world');
assert.equal(map.get(KEY1), 'hello');
assert.equal(map.get(KEY2), 'world');
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:
Different objects are always considered to be different. That is something that can’t be changed (yet – configuring key equality is on TC39’s long term roadmap).
You can .map()
and .filter()
an Array, but there are no such operations for a Map. The solution is:
I’ll use the following Map to demonstrate how that works.
Mapping originalMap
:
const mappedMap = new Map( // step 3
[...originalMap] // step 1
.map(([k, v]) => [k * 2, '_' + v]) // step 2
);
assert.deepEqual([...mappedMap],
[[2,'_a'], [4,'_b'], [6,'_c']]);
Filtering originalMap
:
const filteredMap = new Map( // step 3
[...originalMap] // step 1
.filter(([k, v]) => k < 3) // step 2
);
assert.deepEqual([...filteredMap],
[[1,'a'], [2,'b']]);
Step 1 is performed by spreading (...
) in the Array literal.
There are no methods for combining Maps, which is why we must use a work-around that is similar to the one from the previous section.
Let’s combine the following two Maps:
const map1 = new Map()
.set(1, '1a')
.set(2, '1b')
.set(3, '1c')
;
const map2 = new Map()
.set(2, '2b')
.set(3, '2c')
.set(4, '2d')
;
To combine map1
and map2
, we turn them into Arrays via spreading (...
) and concatenate those Arrays. Afterwards, we convert the result back to a Map. All of that is done in line A.
const combinedMap = new Map([...map1, ...map2]); // (A)
assert.deepEqual(
[...combinedMap], // convert to Array for comparison
[ [ 1, '1a' ],
[ 2, '2b' ],
[ 3, '2c' ],
[ 4, '2d' ] ]
);
Exercise: Combining two Maps
exercises/maps/combine_maps_test.mjs
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
.
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 added as entries to the Map. For example:
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.
.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.
.has(key: K): boolean
[ES6]
Returns whether the given key exists in this Map.
.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.
Map<K,V>.prototype
: handling all entriesget .size: number
[ES6]
Returns how many entries this Map has.
.clear(): void
[ES6]
Removes all entries from this Map.
Map<K,V>.prototype
: iterating and loopingBoth 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.
.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
.
.keys(): Iterable<K>
[ES6]
Returns an iterable over all keys in this Map.
.values(): Iterable<V>
[ES6]
Returns an iterable over all values in this Map.
[Symbol.iterator](): Iterable<[K,V]>
[ES6]
The default way of iterating over Maps. Same as .entries()
.
If you need a dictionary-like data structure with keys that are neither strings nor symbols, you have no choice: you must use a Map.
If, however, your keys are either strings or symbols, 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 obj
and access the values via fixed keys:
Can the set of keys change at runtime?
Then use a Map map
and access the values via keys stored in variables:
You normally want Map keys to be compared by value (two keys are considered equal if they have the same content). That excludes objects. However, there is one use case for objects as keys: externally attaching data to objects. But that use case is served better by WeakMaps, where entries don’t prevent keys from being garbage-collected (for details, consult the next chapter).
In principle, Maps are unordered. The main reason for ordering entries is so that operations that list entries, keys or values, are deterministic. That helps, e.g., with testing.
.size
, while Arrays have a .length
?In JavaScript, indexable sequences (such as Arrays) have a .length
, while mainly unordered collections (such as Maps) have a .size
.
Quiz
See quiz app.