Skip to content

Commit d26a64c

Browse files
committed
split map-set apart from weakmap-weakset
0 parents  commit d26a64c

File tree

5 files changed

+390
-0
lines changed

5 files changed

+390
-0
lines changed

01-recipients-read/solution.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
Let's store read messages in `WeakSet`:
2+
3+
```js
4+
let messages = [
5+
{text: "Hello", from: "John"},
6+
{text: "How goes?", from: "John"},
7+
{text: "See you soon", from: "Alice"}
8+
];
9+
10+
let readMessages = new WeakSet();
11+
12+
// two messages have been read
13+
readMessages.add(messages[0]);
14+
readMessages.add(messages[1]);
15+
// readMessages has 2 elements
16+
17+
// ...let's read the first message again!
18+
readMessages.add(messages[0]);
19+
// readMessages still has 2 unique elements
20+
21+
// answer: was the message[0] read?
22+
alert("Read message 0: " + readMessages.has(messages[0])); // true
23+
24+
messages.shift();
25+
// now readMessages has 1 element (technically memory may be cleaned later)
26+
```
27+
28+
The `WeakSet` allows to store a set of messages and easily check for the existance of a message in it.
29+
30+
It cleans up itself automatically. The tradeoff is that we can't iterate over it, can't get "all read messages" from it directly. But we can do it by iterating over all messages and filtering those that are in the set.
31+
32+
Another, different solution could be to add a property like `message.isRead=true` to a message after it's read. As messages objects are managed by another code, that's generally discouraged, but we can use a symbolic property to avoid conflicts.
33+
34+
Like this:
35+
```js
36+
// the symbolic property is only known to our code
37+
let isRead = Symbol("isRead");
38+
messages[0][isRead] = true;
39+
```
40+
41+
Now third-party code probably won't see our extra property.
42+
43+
Both solutions are possible, though the one with `WeakSet` is "cleaner" from the architectural point of view.

01-recipients-read/task.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
importance: 5
2+
3+
---
4+
5+
# Store "unread" flags
6+
7+
There's an array of messages:
8+
9+
```js
10+
let messages = [
11+
{text: "Hello", from: "John"},
12+
{text: "How goes?", from: "John"},
13+
{text: "See you soon", from: "Alice"}
14+
];
15+
```
16+
17+
Your code can access it, but the messages are managed by someone else's code. New messages are added, old ones are removed regularly by that code, and you don't know the exact moments when it happens.
18+
19+
Now, which data structure you could use to store information whether the message "have been read"? The structure must be well-suited to give the answer "was it read?" for the given message object.
20+
21+
P.S. When a message is removed from `messages`, it should disappear from your structure as well.
22+
23+
P.P.S. We shouldn't modify message objects directly. As they are managed by someone else's code, adding extra properties to them may have bad consequences.

02-recipients-when-read/solution.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
To store a date, we can use `WeakMap`:
3+
4+
```js
5+
let messages = [
6+
{text: "Hello", from: "John"},
7+
{text: "How goes?", from: "John"},
8+
{text: "See you soon", from: "Alice"}
9+
];
10+
11+
let readMap = new WeakMap();
12+
13+
readMap.set(messages[0], new Date(2017, 1, 1));
14+
// Date object we'll study later
15+
```

02-recipients-when-read/task.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
importance: 5
2+
3+
---
4+
5+
# Store read dates
6+
7+
There's an array of messages as in the [previous task](info:task/recipients-read). The situation is similar.
8+
9+
```js
10+
let messages = [
11+
{text: "Hello", from: "John"},
12+
{text: "How goes?", from: "John"},
13+
{text: "See you soon", from: "Alice"}
14+
];
15+
```
16+
17+
The question now is: which data structure you'd suggest to store the information: "when the message was read?".
18+
19+
In the previous task we only needed to store the "yes/no" fact. Now we need to store the date, and it should only remain in memory until the message is garbage collected.

article.md

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
# WeakMap and WeakSet
2+
3+
`WeakSet` is a special kind of `Set` that does not prevent JavaScript from removing its items from memory. `WeakMap` is the same thing for `Map`.
4+
5+
As we know from the chapter <info:garbage-collection>, JavaScript engine stores a value in memory while it is reachable (and can potentially be used).
6+
7+
For instance:
8+
```js
9+
let john = { name: "John" };
10+
11+
// the object can be accessed, john is the reference to it
12+
13+
// overwrite the reference
14+
john = null;
15+
16+
*!*
17+
// the object will be removed from memory
18+
*/!*
19+
```
20+
21+
Usually, properties of an object or elements of an array or another data structure are considered reachable and kept in memory while that data structure is in memory.
22+
23+
For instance, if we put an object into an array, then while the array is alive, the object will be alive as well, even if there are no other references to it.
24+
25+
Like this:
26+
27+
```js
28+
let john = { name: "John" };
29+
30+
let array = [ john ];
31+
32+
john = null; // overwrite the reference
33+
34+
*!*
35+
// john is stored inside the array, so it won't be garbage-collected
36+
// we can get it as array[0]
37+
*/!*
38+
```
39+
40+
Similar to that, if we use an object as the key in a regular `Map`, then while the `Map` exists, that object exists as well. It occupies memory and may not be garbage collected.
41+
42+
For instance:
43+
44+
```js
45+
let john = { name: "John" };
46+
47+
let map = new Map();
48+
map.set(john, "...");
49+
50+
john = null; // overwrite the reference
51+
52+
*!*
53+
// john is stored inside the map,
54+
// we can get it by using map.keys()
55+
*/!*
56+
```
57+
58+
`WeakMap/WeakSet` are fundamentally different in this aspect. They do not prevent garbage-collection of key objects.
59+
60+
Let's explain it starting with `WeakMap`.
61+
62+
## WeakMap
63+
64+
The first difference from `Map` is that `WeakMap` keys must be objects, not primitive values:
65+
66+
```js run
67+
let weakMap = new WeakMap();
68+
69+
let obj = {};
70+
71+
weakMap.set(obj, "ok"); // works fine (object key)
72+
73+
*!*
74+
// can't use a string as the key
75+
weakMap.set("test", "Whoops"); // Error, because "test" is not an object
76+
*/!*
77+
```
78+
79+
Now, if we use an object as the key in it, and there are no other references to that object -- it will be removed from memory (and from the map) automatically.
80+
81+
```js
82+
let john = { name: "John" };
83+
84+
let weakMap = new WeakMap();
85+
weakMap.set(john, "...");
86+
87+
john = null; // overwrite the reference
88+
89+
// john is removed from memory!
90+
```
91+
92+
Compare it with the regular `Map` example above. Now if `john` only exists as the key of `WeakMap` -- it will be automatically deleted from the map (and memory).
93+
94+
`WeakMap` does not support iteration and methods `keys()`, `values()`, `entries()`, so there's no way to get all keys or values from it.
95+
96+
`WeakMap` has only the following methods:
97+
98+
- `weakMap.get(key)`
99+
- `weakMap.set(key, value)`
100+
- `weakMap.delete(key)`
101+
- `weakMap.has(key)`
102+
103+
Why such a limitation? That's for technical reasons. If an object has lost all other references (like `john` in the code above), then it is to be garbage-collected automatically. But technically it's not exactly specified *when the cleanup happens*.
104+
105+
The JavaScript engine decides that. It may choose to perform the memory cleanup immediately or to wait and do the cleaning later when more deletions happen. So, technically the current element count of a `WeakMap` is not known. The engine may have cleaned it up or not, or did it partially. For that reason, methods that access all keys/values are not supported.
106+
107+
Now where do we need such data structure?
108+
109+
## Use case: additional data
110+
111+
The main area of application for `WeakMap` is an *additional data storage*.
112+
113+
There are objects managed elsewhere in the code, maybe they come from a third-party code, and in our code we need to keep additional information that is only relevant while the object is in memory.
114+
115+
And when the object is garbage collected, that data should automatically disappear as well.
116+
117+
```js
118+
weakMap.set(john, "secret documents");
119+
// if john dies, secret documents will be destroyed automatically
120+
```
121+
122+
Let's look at an example.
123+
124+
For instance, we have code that keeps a visit count for each user. The information is stored in a map: a user object is the key and the visit count is the value. When a user leaves (its object gets garbage collected), we don't want to store their visit count anymore.
125+
126+
Here's an example of a counting function with `Map`:
127+
128+
```js
129+
// 📁 visitsCount.js
130+
let visitsCountMap = new Map(); // map: user => visits count
131+
132+
// increase the visits count
133+
function countUser(user) {
134+
let count = visitsCountMap.get(user) || 0;
135+
visitsCountMap.set(count + 1);
136+
}
137+
```
138+
139+
Let's imagine another part of the code using it:
140+
141+
```js
142+
// 📁 main.js
143+
let john = { name: "John" };
144+
145+
countUser(john); // count his visits
146+
countUser(john);
147+
148+
// later john leaves us
149+
john = null;
150+
```
151+
152+
Now, we have a problem: `john` object should be garbage collected, but remains is memory, as it's a key in `visitsCountMap`.
153+
154+
We need to clean up `visitsCountMap` when we remove users, otherwise it will grow in memory indefinitely. Such cleaning can become a tedious task in complex architectures.
155+
156+
We can avoid it by switching to `WeakMap` instead:
157+
158+
```js
159+
// 📁 visitsCount.js
160+
let visitsCountMap = new WeakMap(); // map: user => visits count
161+
162+
// increase the visits count
163+
function countUser(user) {
164+
let count = visitsCountMap.get(user) || 0;
165+
visitsCountMap.set(count + 1);
166+
}
167+
```
168+
169+
Now we don't have to clean `visitsCountMap`. After `john` is removed from memory, the additionally stored information from `WeakMap` will be removed as well.
170+
171+
## Use case: caching
172+
173+
Another common example is caching: when a function result should be remembered ("cached"), so that future calls on the same object reuse it.
174+
175+
We can use `Map` for it, like this:
176+
177+
```js run
178+
// 📁 cache.js
179+
let cache = new Map();
180+
181+
// calculate and remember the result
182+
function process(obj) {
183+
if (!cache.has(obj)) {
184+
let result = /* calculate the result for */ obj;
185+
186+
cache.set(obj, result);
187+
}
188+
189+
return cache.get(obj);
190+
}
191+
192+
*!*
193+
// Usage in another file:
194+
*/!*
195+
// 📁 main.js
196+
let obj = {/* some object */};
197+
198+
let result1 = process(obj); // calculated
199+
200+
// ...later, from another place of the code...
201+
let result2 = process(obj); // taken from cache
202+
203+
// ...later, when the object is not needed any more:
204+
obj = null;
205+
206+
alert(cache.size); // 1 (Ouch! It's still in cache, taking memory!)
207+
```
208+
209+
Now for multiple calls of `process(obj)` with the same object, it only calculates the result the first time, and then just takes it from `cache`. The downside is that we need to clean `cache` when the object is not needed any more.
210+
211+
If we replace `Map` with `WeakMap`, then the cached result will be removed from memory automatically after the object gets garbage collected:
212+
213+
```js run
214+
// 📁 cache.js
215+
*!*
216+
let cache = new WeakMap();
217+
*/!*
218+
219+
// calculate and remember the result
220+
function process(obj) {
221+
if (!cache.has(obj)) {
222+
let result = /* calculate the result for */ obj;
223+
224+
cache.set(obj, result);
225+
}
226+
227+
return cache.get(obj);
228+
}
229+
230+
// 📁 main.js
231+
let obj = {/* some object */};
232+
233+
let result1 = process(obj);
234+
let result2 = process(obj);
235+
236+
// ...later, when the object is not needed any more:
237+
obj = null;
238+
239+
// Can't get cache.size, as it's a WeakMap, but it's 0 or soon be 0
240+
// When obj gets garbage collected, cached data will be removed as well
241+
```
242+
243+
## WeakSet
244+
245+
`WeakSet` behaves similarly:
246+
247+
- It is analogous to `Set`, but we may only add objects to `WeakSet` (not primitives).
248+
- An object exists in the set while it is reachable from somewhere else.
249+
- Like `Set`, it supports `add`, `has` and `delete`, but not `size`, `keys()` and no iterations.
250+
251+
Being "weak", it also serves as an additional storage. But not for an arbitrary data, but rather for "yes/no" facts. A membership in `WeakSet` may mean something about the object.
252+
253+
For instance, we can use `WeakSet` to keep track of users that visited our site:
254+
255+
```js run
256+
let visitedSet = new WeakSet();
257+
258+
let john = { name: "John" };
259+
let pete = { name: "Pete" };
260+
let mary = { name: "Mary" };
261+
262+
visitedSet.add(john); // John visited us
263+
visitedSet.add(pete); // Then Pete
264+
visitedSet.add(john); // John again
265+
266+
// visitedSet has 2 users now
267+
268+
// check if John visited?
269+
alert(visitedSet.has(john)); // true
270+
271+
// check if Mary visited?
272+
alert(visitedSet.has(mary)); // false
273+
274+
// John object is not needed any more
275+
john = null;
276+
277+
// visitedSet will be cleaned automatically
278+
```
279+
280+
The most notable limitation of `WeakMap` and `WeakSet` is the absence of iterations, and inability to get all current content. That may appear inconvenient, but does not prevent `WeakMap/WeakSet` from doing their main job -- be an "additional" storage of data for objects which are stored/managed at another place.
281+
282+
## Summary
283+
284+
`WeakMap` is `Map`-like collection that allows only objects as keys and removes them once they become inaccessible by other means.
285+
286+
`WeakSet` is `Set`-like collection that only stores objects and removes them once they become inaccessible by other means.
287+
288+
Both of them do not support methods and properties that refer to all keys or their count. Only individial get/has/set/remove operations with a given key are allowed.
289+
290+
`WeakMap` and `WeakSet` are used as "secondary" data structures in addition to the "main" object storage. Once the object is removed from the main storage, if it is only found as the key of `WeakMap` or in a `WeakSet`, it will be cleaned up automatically.

0 commit comments

Comments
 (0)