WeakMap
and WeakSet
: Understanding JavaScript weak referencesWeak references are not often used in JavaScript due to how the language is designed. However, in certain circumstances, they can prove vital, such as when developers must store additional data and need to automatically manage the memory usage.
In this post, we’ll learn about these weak references in JavaScript and how we can use them by utilizing two objects within the language: WeakMap
and WeakSet
.
Let’s start by taking a look at what a normal, or strong, reference is in JavaScript. In its simplest definition, a strong reference is a reference that keeps an object in memory. Let’s take a look at this in practice to understand what we are talking about:
let dog = { name: "badger" }; const pets = [dog]; dog = null; console.log(pets); // [{ name: "badger" }]
By creating a variable as an object, we can place the object into an array and remove the reference to the original object from the variable we created by setting its value to null
.
Although we can’t access the object via the dog
variable anymore because there is a strong reference between the pets
array and the object, the object is kept in memory and can be accessed via pets[0]
.
In other words, the strong reference prevents removing the object from memory via garbage collection.
Simply put a weak reference is a reference to an object that doesn’t prevent garbage collection if it is the only reference to the object in the memory.
A normal reference (considered strong), would prevent the garbage collection of an object even if it is the only object referencing it; this isn’t the case for a weak reference.
Let’s take this theory and put it into practice with the previous example of a strong reference and putting it into the context of a weak reference. Ignore the use of WeakMap
right now; we will explain this in more depth later in the article. For now, let’s see weak reference behavior:
let pets = new WeakMap(); let dog = { name: "badger" }; pets.set(dog, "okay"); console.log(pets); // WeakMap{ {...} -> "Okay" } <= dog set to the WeakMap dog = null; // Overwrite the reference to the object console.log(pets); // WeakMap(0) <= dog has been garbage collected.
By utilizing WeakMap
and the weak references that come with it, we can see the differences between the two types of references in action. While the strong (normal) reference to the original dog
object still exists, the dog
object persists in the WeakMap
, and we can access it with no issues.
But, when we overwrite the reference to the original dog
object by reassigning the variable to null
, the only reference to the original object in memory is the weak reference coming from the WeakMap
we created.
Because it’s a weak reference, it won’t prevent garbage collection from occurring. This means when the JavaScript engine runs a garbage collection process again, the dog
object will be removed from memory and from the WeakMap
we assigned it to.
The key difference to note is that a strong reference prevents an object from garbage collection while a weak one will not.
By default, JavaScript uses strong references for all of its references and the only way to use weak references is to use either a WeakMap
or a WeakSet
.
While garbage collection is a detailed and complicated subject, it is important to understand when discussing references.
Garage collection is an automated process controlled by the JavaScript engine. When a value is reachable, it is guaranteed to be stored in memory and not garbage collected, and there are two ways a value is considered reachable.
This first is that they are part of the base set of reachable values like global variables, the current executing function and its local variables/parameters, and more internal values.
The other is reaching any value from the root by reference or a chain of references. For instance, imagine we create an object in a global variable; this is reachable by the global space, thus considered reachable.
Now, if we create another object and reference it off the global object we created, it is also reachable because it’s referenced via the global object.
However, if we remove the global object by setting it to null
, suddenly the one we could reach by reference isn’t reachable, so it would be garbage collected.
This is specifically referencing strong references because they are the default in JavaScript. But, the same does apply to weak references, the only exception being if the only reference to an object is weak, it does not prevent garbage collection, and the object is removed.
That is a high-level overview of how garbage collection works; essentially, if something isn’t reachable, it is removed from memory so the memory can be used in other locations.
Sets
vs. WeakSets
Per MDN, “Set
objects are collections of values. You can iterate through the elements of a set in insertion order. A value in the Set
may only occur once; it is unique in the Set
‘s collection.”
Simply put, a Set
is like an array that can only contain unique values but we can still iterate through it like an array using methods like for loops and .forEach
.
Similar to a Set
, WeakSet
is a collection of objects that are unique from each other but differs because WeakSet
can only store objects and cannot contain arbitrary values of any type like strings or numbers.
Ultimately, as the name suggests, WeakSets
are indeed weak, meaning they use weak references.
It is also worth noting an interesting side effect of using weak references is that WeakSet
is not enumerable. This means there is no way to loop over the items contained within it because there is no list of current objects stored in the collection; they are weakly referenced and may be removed at any point.
Here is an example of WeakSet
in use and the methods we can call on it:
const pets = new WeakSet(); const cat = {name: "fluffy"}; const dog = {name: "badger"}; pets.add(cat); pets.add(dog); pets.has(cat); // true pets.has(dog); // true pets.delete(cat); // removes cat from the set pets.has(cat); // false, cat has been removed pets.has(dog); // true, dog is retained
Maps
vs. WeakMap
According to MDN, “The Map
object holds key-value pairs and remembers the original insertion order of the keys. Any value (both objects and primitive values) may be used as either a key or a value.”
This means a Map
is like an object where we can store key-value pairs and access the values contained within the Map
through the key. Unlike a standard object in JavaScript, however, we must use the .get()
method to access the values.
In comparison to a Map
, a WeakMap
is very much the same but the references it holds are weak references, meaning it won’t prevent garbage collection from removing values it references if they are not strongly referenced elsewhere.
Also, WeakMap
has the same side effect of not being enumerable due to the weak references.
Finally, we must use objects as the keys, but the values can be any arbitrary value like a string or number. Here is an example of WeakMaps
used and the methods we can use on it:
const wm1 = new WeakMap(); const wm2 = new WeakMap(); const obj1 = {}; const obj2 = window; wm1.set(obj1, 100); wm1.set(obj2, 'Hello'); wm2.set(obj1, obj2); // You can set the value to be anything including an object or function wm2.set(obj2, undefined); // Or, undefined wm1.set(wm2, wm1); // Or, even a WeakMap itself wm1.get(obj1); // 100 wm1.has(obj1); // true wm1.delete(obj1); wm1.has(obj1); // false
Before closing out, let’s consider a potential use case for weak references and the two objects we covered in this article.
If you need to store additional data temporarily and don’t want to worry about cleaning up the memory or how the objects are removed, then using weak references is an absolute lifesaver.
But, it’s not likely that you will regularly need to use WeakMaps
, WeakSets
, or even weak references regularly in JavaScript.
They are handy to know for the occasional situation and great to have base knowledge about, but in the majority of situations, use normal (strong) references.
I hope you found this article on weak references in JavaScript helpful, if you did, please consider following me over on Twitter, where I post helpful and actionable tips and content on the JavaScript ecosystem.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.