Working with Maps and WeakMaps Flashcards
Maps and Sets introduction
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.
- The keys of an
Object
arestrings
orsymbols
, whereas they can be of any value for aMap
. - You can get the size of a
Map
easily, while you have to manually keep track of size for anObject
. - The iteration of maps is in insertion order of the elements.
- An
Object
has a prototype, so there are default keys in the map. (This can be bypassed usingmap = 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:
- Use maps over objects when keys are unknown until run time, and when all keys are the same type and all values are the same type.
- Use maps if there is a need to store primitive values as keys because object treats each key as a string whether it’s a
number
value,boolean
value or any other primitive value. - Use objects when there is logic that operates on individual elements.
“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
:
- does not prevent garbage collection, which eventually removes references to the key object
- allows garbage collection of any values if their key objects are not referenced from somewhere other than 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 WeakMap
s include:
- Keeping private data about a specific object and only giving access to it to people with a reference to the Map.
- Keeping data about library objects without changing them or incurring overhead.
- Keeping data about host objects like DOM nodes in the browser.
Adding a capability to an object from the outside.
“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:
- The
clicked
property is enumerable, so it will show up inObject.keys(button)
,for...in
loops, etc. This can be mitigated by usingObject.defineProperty()
, but that makes the code more verbose. - The
clicked
property is a normal string property, so it can be accessed and overwritten by other code. This can be mitigated by using aSymbol
key, but the key would still be accessible viaObject.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 WeakMap
s, 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 WeakMap
s” (exploringjs.com). Retrieved May 1, 2024.
How can you keep private data on WeakMap
s?
In the following code, the WeakMap
s _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 WeakMap
s” 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, WeakMap
s 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 WeakMap
s are black boxes
It is impossible to inspect what’s inside a WeakMap
:
For example,
- you can’t iterate or loop over
keys
,values
orentries
. And you can’t compute the size. - Additionally, you can’t clear a
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.
“WeakMas
s are black boxes” Retrieved May 1, 2024.
How do you create a Map
?
There are three common ways of creating Map
s.
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
?
Map
s 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 // true
We 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.