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