Understanding Weak Reference In JavaScript

Understanding Weak Reference In JavaScript

Memory and performance management are important aspects of software development and ones that every software developer should pay attention to. Though useful, weak references are not often used in JavaScript. WeakSet and WeakMap were introduced to JavaScript in the ES6 version.

Weak Reference

To clarify, unlike strong reference, weak reference doesn’t prevent the referenced object from being reclaimed or collected by the garbage collector, even if it is the only reference to the object in memory.

Before getting into strong reference, WeakSet, Set, WeakMap, and Map, let’s illustrate weak reference with the following snippet:

// Create an instance of the WeakMap object.
let human = new WeakMap():

// Create an object, and assign it to a variable called man.
let man = { name: "Joe Doe" };

// Call the set method on human, and pass two arguments (key and value) to it.
human.set(man, "done")

console.log(human)

The output of the code above would be the following:

WeakMap {{…} => 'done'}

man = null;
console.log(human)

The man argument is now set to the WeakMap object. At the point when we reassigned the man variable to null, the only reference to the original object in memory was the weak reference, and it came from the WeakMap that we created earlier. When the JavaScript engine runs a garbage-collection process, the man object will be removed from memory and from the WeakMap that we assigned it to. This is because it is a weak reference, and it doesn’t prevent garbage collection.

It looks like we are making progress. Let’s talk about strong reference, and then we’ll tie everything together.

Strong Reference

A strong reference in JavaScript is a reference that prevents an object from being garbage-collected. It keeps the object in memory.

The following code snippets illustrate the concept of strong reference:

let man = {name: "Joe Doe"};

let human = [man];

man =  null;
console.log(human);

The result of the code above would be this:

// An array of objects of length 1. 
[{…}]

The object cannot be accessed via the dog variable anymore due to the strong reference that exists between the human array and object. The object is retained in memory and can be accessed with the following code:

console.log(human[0])

The important point to note here is that a weak reference doesn’t prevent an object from being garbage-collected, whereas a strong reference does prevent an object from being garbage-collected.

Garbage Collection in JavaScript

As in every programming language, memory management is a key factor to consider when writing JavaScript. Unlike C, JavaScript is a high-level programming language that automatically allocates memory when objects are created and that clears memory automatically when the objects are no longer needed. The process of clearing memory when objects are no longer being used is referred to as garbage collection. It is almost impossible to talk about garbage collection in JavaScript without touching on the concept of reachability.

Reachability

All values that are within a specific scope or that are in use within a scope are said to be “reachable” within that scope and are referred to as “reachable values”. Reachable values are always stored in memory.

Values are considered reachable if they are:

  • values in the root of the program or referenced from the root, such as global variables or the currently executing function, its context, and callback;
  • values accessible from the root by a reference or chain of references (for example, an object in the global variable referencing another object, which also references another object — these are all considered reachable values).

The code snippets below illustrate the concept of reachability:

let languages = {name: “JavaScript”};

Here we have an object with a key-value pair (with the name JavaScript) referencing the global variable languages. If we overwrite the value of languages by assigning null to it…

languages = null;

… then the object will be garbage-collected, and the value JavaScript cannot be accessed again. Here is another example:

let languages = {name: “JavaScript”};

let programmer = languages;

From the code snippets above, we can access the object property from both the languages variable and the programmer variable. However, if we set languages to null

languages = null;

… then the object will still be in memory because it can be accessed via the programmer variable. This is how garbage collection works in a nutshell.

Note: By default, JavaScript uses strong reference for its references. To implement weak reference in JavaScript, you would use WeakMap, WeakSet, or WeakRef.

Comparing Set and WeakSet

A set object is a collection of unique values with a single occurrence. A set, like an array, does not have a key-value pair. We can iterate through a set of arrays with the array methods for… of and .forEach.

Let’s illustrate this with the following snippets:

let setArray = new Set(["Joseph", "Frank", "John", "Davies"]);
for (let names of setArray){
  console.log(names)
}// Joseph Frank John Davies

We can use the .forEach iterator as well:

 setArray.forEach((name, nameAgain, setArray) =>{
   console.log(names);
 });

A WeakSet is a collection of unique objects. As the name applies, WeakSets use weak reference. The following are properties of WeakSet():

  • It may only contain objects.
  • Objects within the set can be reachable somewhere else.
  • It cannot be looped through.
  • Like Set(), WeakSet() has the methods add, has, and delete.

The code below illustrates how to use WeakSet() and some of the methods available:

const human = new WeakSet();

let paul = {name: "Paul"};
let mary = {gender: "Mary"};

// Add the human with the name paul to the classroom. 
const classroom = human.add(paul);

console.log(classroom.has(paul)); // true

paul = null;

// The classroom will be cleaned automatically of the human paul.

console.log(classroom.has(paul)); // false

On line 1, we’ve created an instance of WeakSet(). On lines 3 and 4, we created objects and assigned them to their respective variables. On line 7, we added paul to the WeakSet() and assigned it to the classroom variable. On line 11, we made the paul reference null. The code on line 15 returns false because WeakSet() will be automatically cleaned; so, WeakSet() doesn’t prevent garbage collection.

Comparing Map and WeakMap

As we know from the section on garbage collection above, the JavaScript engine keeps a value in memory as long as it is reachable. Let’s illustrate this with some snippets:

let smashing = {name: "magazine"};
// The object can be accessed from the reference.

// Overwrite the reference smashing.
smashing = null;
// The object can no longer be accessed.

Properties of a data structure are considered reachable while the data structure is in memory, and they are usually kept in memory. If we store an object in an array, then as long as the array is in memory, the object can still be accessed even if it has no other references.

let smashing = {name: "magazine"};

let arr = [smashing];

// Overwrite the reference.
smashing = null;
console.log(array[0]) // {name: 'magazine'}

We’re still able to access this object even if the reference has been overwritten because the object was saved in the array; hence, it was saved in memory as long the array is still in memory. Therefore, it was not garbage-collected. As we’ve used an array in the example above, we can use map too. While the map still exists, the values stored in it won’t be garbage-collected.

let map = new Map();

let smashing {name: "magazine"};

map.set(smashing, "blog");

// Overwrite the reference.
smashing = null;

// To access the object.
console.log(map.keys());

Like an object, maps can hold key-value pairs, and we can access the value through the key. But with maps, we must use the .get() method to access the values.

According to Mozilla Developer Network, 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 key or value.

Unlike a map, WeakMap holds a weak reference; hence, it doesn’t prevent garbage collection from removing values that it references if those values are not strongly referenced elsewhere. Apart from this, WeakMap is the same as map. WeakMaps are not enumerable due to weak references.

With WeakMap, the keys must be objects, and the values may be a number or a string.

The snippets below illustrate how WeakMap works and the methods in it:

// Create a weakMap.
let weakMap = new WeakMap();

let weakMap2 = new WeakMap();

// Create an object.
let ob = {};

// Use the set method.
weakMap.set(ob, "Done");

// You can set the value to be an object or even a function.
weakMap.set(ob, ob)

// You can set the value to undefined.
weakMap.set(ob, undefined);

// WeakMap can also be the value and the key.
weakMap.set(weakMap2, weakMap)

// To get values, use the get method.
weakMap.get(ob) // Done

// Use the has method.
weakMap.has(ob) // true

weakMap.delete(ob)

weakMap.has(ob) // false

One major side effect of using objects as keys in a WeakMap with no other references to it is that they will be automatically removed from memory during garbage collection.

Areas of Application of WeakMap

WeakMap can be used in two areas of web development: caching and additional data storage.

Caching

This a web technique that involves saving (i.e. storing) a copy of a given resource and serving it back when requested. The result from a function can be cached so that whenever the function is called, the cached result can be reused.

Let’s see this in action. Create a file, name it cachedResult.js, and write the following in it:

 let cachedResult = new WeakMap();
 // A function that stores a result.
function keep(obj){
if(!cachedResult.has(obj){
  let result = obj;
  cachedResult.set(obj, result);
  }
return cachedResult.get(obj);
}


let obj = {name: "Frank"};

let resultSaved = keep(obj)

obj = null;

// console.log(cachedResult.size); Possible with map, not with WeakMap

If we had used Map() instead of WeakMap() in the code above, and there were multiple invocations on the function keep(), then it would only calculate the result the first time it was called, and it would retrieve it from cachedResult the other times. The side effect is that we’ll need to clean cachedResult whenever the object is not needed. With WeakMap(), the cached result will be automatically removed from memory as soon as the object is garbage-collected. Caching is a great means of improving software performance — it could save the costs of database usage, third-party API calls, and server-to-server requests. With caching, a copy of the result from a request is saved locally.

Additional Data

Another important use of WeakMap() is additional data storage. Imagine we are building an e-commerce platform, and we have a program that counts visitors, and we want to be able to reduce the count when visitors leave. This task would be very demanding with Map, but quite easy to implement with WeakMap():

let visitorCount = new WeakMap();
function countCustomer(customer){
   let count = visitorCount.get(customer) || 0;
    visitorCount.set(customer, count + 1);
}

Let’s create client code for this:

let person = {name: "Frank"};

// Taking count of person visit.
countCustomer(person)

// Person leaves.
person = null;

With Map(), we will have to clean visitorCount whenever a customer leaves; otherwise, it will grow in memory indefinitely, taking up space. But with WeakMap(), we do not need to clean visitorCount; as soon as a person (object) becomes unreachable, it will be garbage-collected automatically.

Conclusion

In this article, we learned about weak reference, strong reference, and the concept of reachability, and we tried to connect them to memory management as best we could. I hope you found this article valuable. Feel free to drop a comment.