What is a Map object?
A Map object is a simple key/value map and can iterate its elements in insertion order.
The following code shows some basic operations with a Map. You can use a for...of loop to return an array of [key, value] for each iteration.
const sayings = new Map();
sayings.set("dog", "woof");
sayings.set("cat", "meow");
sayings.set("elephant", "toot");
sayings.size; // 3
sayings.get("dog"); // woof
sayings.get("fox"); // undefined
sayings.has("bird"); // false
sayings.delete("dog");
sayings.has("dog"); // false
for (const [key, value] of sayings) {
console.log(`${key} goes ${value}`);
}
// "cat goes meow"
// "elephant goes toot"
sayings.clear();
sayings.size; // 0“Map object” (MDN Web Docs). Retrieved April 24, 2024.
Advantages of using Map objects over Object to store key/value pairs?
Traditionally, objects have been used to map strings to values. Objects allow you to set keys to values, retrieve those values, delete keys, and detect whether something is stored at a key. Map objects, however, have a few more advantages that make them better maps.
Object are strings or symbols, whereas they can be of any value for a Map.Map easily, while you have to manually keep track of size for an Object.Object has a prototype, so there are default keys in the map. (This can be bypassed using map = Object.create(null).)When should you use Map object over an Object?
These three tips can help you to decide whether to use a Map or an Object:
number value, boolean value or any other primitive value.“Object and Map compared” (MDN Web Docs). Retrieved April 24, 2024.
What is a WeakMap object?
A WeakMap is a map (dictionary) where the keys are weak - that is, if all references to the key are lost and there are no more references to the value - the value can be garbage collected.
The only primitive type that can be used as a WeakMap key is symbol — more specifically, non-registered symbols — because non-registered symbols are guaranteed to be unique and cannot be re-created.
The WeakMap API is essentially the same as the Map API. However, a WeakMap doesn’t allow observing the liveness of its keys, which is why it doesn’t allow enumeration. So there is no method to obtain a list of the keys in a WeakMap. If there were, the list would depend on the state of garbage collection, introducing non-determinism.
“WeakMap object” (MDN Web Docs). Retrieved April 25, 2024.
“Why WeakMap?
In a WeakMap, a key object refers strongly to its contents as long as the key is not garbage collected, but weakly from then on. As such, a WeakMap:
A WeakMap can be a particularly useful construct when mapping keys to information about the key that is valuable only if the key has not been garbage collected.
But because a WeakMap doesn’t allow observing the liveness of its keys, its keys are not enumerable. There is no method to obtain a list of the keys. If there were, the list would depend on the state of garbage collection, introducing non-determinism. If you want to have a list of keys, you should use a Map.
“Why WeakMap?” (MDN Web Docs). Retrieved April 27, 2024.
List some WeakMap use cases
Use cases
Some use cases that would otherwise cause a memory leak and are enabled by WeakMaps include:
“What are the actual uses of ES6 WeakMap?” (Stack Overflow). Retrieved April 27, 2024.
How can you use a WeakMap to associate metadata to an object?
A WeakMap can be used to associate metadata with an object, without affecting the lifetime of the object itself.
For example, on the web, we may want to associate extra data with a DOM element, which the DOM element may access later. A common approach is to attach the data as a property:
const buttons = document.querySelectorAll(".button");
buttons.forEach((button) => {
button.clicked = false;
button.addEventListener("click", () => {
button.clicked = true;
const currentButtons = [...document.querySelectorAll(".button")];
if (currentButtons.every((button) => button.clicked)) {
console.log("All buttons have been clicked!");
}
});
});This approach works, but it has a few pitfalls:
clicked property is enumerable, so it will show up in Object.keys(button), for...in loops, etc. This can be mitigated by using Object.defineProperty(), but that makes the code more verbose.clicked property is a normal string property, so it can be accessed and overwritten by other code. This can be mitigated by using a Symbol key, but the key would still be accessible via Object.getOwnPropertySymbols().Using a WeakMap fixes these:
const buttons = document.querySelectorAll(".button");
const clicked = new WeakMap();
buttons.forEach((button) => {
clicked.set(button, false);
button.addEventListener("click", () => {
clicked.set(button, true);
const currentButtons = [...document.querySelectorAll(".button")];
if (currentButtons.every((button) => clicked.get(button))) {
console.log("All buttons have been clicked!");
}
});
});Here, only code that has access to clicked knows the clicked state of each button, and external code can’t modify the states. In addition, if any of the buttons gets removed from the DOM, the associated metadata will automatically get garbage-collected.
“Associating metadata” (MDN Web Docs). Retrieved April 30, 2024.
How can you use WeakMap for caching?
With WeakMaps, you can associate previously computed results with objects without having to worry about memory management. The following function countOwnKeys() is an example: it caches previous results in the WeakMap cache.
const cache = new WeakMap();
function countOwnKeys(obj) {
if (cache.has(obj)) {
return [cache.get(obj), 'cached'];
} else {
const count = Object.keys(obj).length;
cache.set(obj, count);
return [count, 'computed'];
}
}If we use this function with an object obj, you can see that the result is only computed for the first invocation, while a cached value is used for the second invocation:
> const obj = { foo: 1, bar: 2};
> countOwnKeys(obj)
[2, 'computed']
> countOwnKeys(obj)
[2, 'cached']“Caching computed results via WeakMaps” (exploringjs.com). Retrieved May 1, 2024.
How can you keep private data on WeakMaps?
In the following code, the WeakMaps _counter and _action are used to store the values of virtual properties of instances of Countdown:
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
// The two pseudo-properties are truly private:
assert.deepEqual(
Object.keys(new Countdown()),
[]);This is how Countdown is used:
let invoked = false; const cd = new Countdown(3, () => invoked = true); cd.dec(); assert.equal(invoked, false); cd.dec(); assert.equal(invoked, false); cd.dec(); assert.equal(invoked, true)
“Keeping private data in WeakMaps” Retrieved May 1, 2024.
What is the problem with this code?
const wm = new WeakMap(); wm.set(123, 'test')
All WeakMap keys must be objects. You get an error if you use a primitive value:
> const wm = new WeakMap(); > wm.set(123, 'test') TypeError: Invalid value used as weak map key
With primitive values as keys, WeakMaps wouldn’t be black boxes anymore. But given that primitive values are never garbage-collected, you don’t profit from weakly held keys anyway, and can just as well use a normal Map.
“All WeakMap keys must be objects” Retrieved May 1, 2024.
Explain why WeakMaps are black boxes
It is impossible to inspect what’s inside a WeakMap:
For example,
keys, values or entries. And you can’t compute the size.WeakMap either – you have to create a fresh instance.These restrictions enable a security property. Quoting Mark Miller:
The mapping from weakmap/key pair value can only be observed or affected by someone who has both the weakmap and the key. With clear(), someone with only the WeakMap would’ve been able to affect the WeakMap-and-key-to-value mapping.
“WeakMass are black boxes” Retrieved May 1, 2024.
How do you create a Map?
There are three common ways of creating Maps.
First, you can use the constructor without any parameters to create an empty Map:
const emptyMap = new Map(); assert.equal(emptyMap.size, 0);
Second, you can pass an iterable (e.g., an Array) over key-value “pairs” (Arrays with two elements) to the constructor:
const map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], // trailing comma is ignored ]);
Third, the .set() method adds entries to a Map and is chainable:
const map = new Map() .set(1, 'one') .set(2, 'two') .set(3, 'three');
“Creating Maps” (exploringjs.com). Retrieved May 2, 2024.
How can you copy a Map?
Maps are also iterables over key-value pairs. Therefore, you can use the constructor to create a copy of a Map. That copy is shallow: keys and values are the same; they are not duplicated.
const original = new Map() .set(false, 'no') .set(true, 'yes'); const copy = new Map(original); assert.deepEqual(original, copy);
“Copying Maps” (exploringjs.com). Retrieved May 2, 2024.
How can you get, set, delete and check key-value pairs in a Map?
.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)“Working with single entries” (exploringjs.com). Retrieved May 2, 2024.
How can you get the keys and values of a Map?
.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
// trueWe use Array.from() to convert the iterable returned by .keys() to an Array:
assert.deepEqual( Array.from(map.keys()), [false, true]);
.values() works like .keys(), but for values instead of keys.
“Getting the keys and values of a Map” Retrieved May 3, 2024.
How can you get the entries of a Map?
.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']Array.from() converts the iterable returned by .entries() to an Array:
assert.deepEqual(
Array.from(map.entries()),
[[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:
for (const [key, value] of map) {
console.log(key, value);
}
// Output:
// false, 'no'
// true, 'yes' “Getting the entries of a Map” Retrieved May 3, 2024.
How can you convert between Maps and Objects?
As long as a Map only uses strings and symbols as keys, you can convert it to an object (via Object.fromEntries()):
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.entries()):
const obj = {
a: 1,
b: 2,
};
const map = new Map(Object.entries(obj));
assert.deepEqual(
map, new Map([['a', 1], ['b', 2]])); “Converting between Maps and Objects” Retrieved May 3, 2024.
What values can you set as a Map key?
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');“A few more details about the keys of Maps (advanced)” (exploringjs.com). Retrieved May 4, 2024.
What keys are considered equal in a Map?
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:
> const map = new Map(); > map.set(NaN, 123); > map.get(NaN) 123
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).
> new Map().set({}, 1).set({}, 2).size
2“What keys are considered equal?” (exploringjs.com). Retrieved May 4, 2024.
How can you map and filter a Map?
You can .map() and .filter() an Array, but there are no such operations for a Map. The solution is:
Map to an Array of [key, value] pairs.Map.I’ll use the following Map to demonstrate how that works.
const originalMap = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c'); Mapping originalMap: const mappedMap = new Map( // step 3 Array.from(originalMap) // step 1 .map(([k, v]) => [k * 2, '_' + v]) // step 2 ); assert.deepEqual( Array.from(mappedMap), [[2,'_a'], [4,'_b'], [6,'_c']]); Filtering originalMap: const filteredMap = new Map( // step 3 Array.from(originalMap) // step 1 .filter(([k, v]) => k < 3) // step 2 ); assert.deepEqual(Array.from(filteredMap), [[1,'a'], [2,'b']]); Array.from() converts any iterable to an Array.
“Mapping and filtering Maps” (exploringjs.com). Retrieved May 4, 2024.
How can you combine 2 Maps?
There are no methods for combining Maps, which is why we must use a workaround.
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 create a new Array and spread (...) the entries (key-value pairs) of map1 and map2 into it (via iteration). Then we convert the Array back into a Map. All of that is done in line A:
const combinedMap = new Map([...map1, ...map2]); // (A)
assert.deepEqual(
Array.from(combinedMap), // convert to Array for comparison
[ [ 1, '1a' ],
[ 2, '2b' ],
[ 3, '2c' ],
[ 4, '2d' ] ]“Combining Maps” (exploringjs.com). Retrieved May 6, 2024.