Understanding JavaScript WeakMaps & WeakSets

Note: This post is work-in-progress learning-note and still in active development and updated regularly.

The JavaScript (JS) WeakMaps and WeakSets are similar to Maps & Sets objects. In previous learning-note post, Learning JavaScript Maps & Sets some basic features of JS Maps and Sets : basic syntax, creating an maps & sets, how they differ with objects & arrays, respectively were discussed. In this learning-post, we will discuss

WeakMap

The WeakMap object is defined in MDN as “ a collection of key/value pairs in which the keys are weakly referenced. The keys must be objects and the values can be arbitrary values”. It is similar to Map but with fewer methods and weak because non-essential objects can be cleared from the memory (garbage collection).

One of the major difference between the Map and WeakMap objects is that WeakMaps are not enumerable, meaning there is no-way to print a list of the keys, which is possible in Map objects.

Using WeakMap Object

One of the key features of WeakMap is that only objects can be used keys, but values can be any arbitrary values as shown below:

// define variables
let myTrucks = {}; // empty object
let model = "Toyota"; // a string
let year = 2018; // a number
let make = function() {}; // a function
 
// initialize myVehicles WeakMap
let myVehicles = new WeakMap();
let myCars = new WeakMap();

//add veriable items to WeakMap object
myVehicles.set(myTrucks, "Ford"); // => WeakMap {{…} => "Ford"}
myVehicles.set(model, "Toyota"); // => TypeError - Invalid value
myVehicles.set(year, 2018); // => TypeError - Invalid value
//value can be a function
myVehicles.set(myTrucks, make); // => WeakMap {{…} => ƒ}
//key can be a weakMap object
myVehicles.set(myCars, myTrucks); //=> WeakMap {{…} => ƒ, WeakMap => {…}}

In the example above, four variables: an empty object, a string, a number, and a function, are initialized (lines: 2-5). In lines 8-9, two WeakMap objects: myVehicles and myCars are initialized using new WeakMap() constructor function.

Only an object (eg. myTrucks, line: 12), a function (eg. make), or even a WeakMap object (eg. myCars, line: 18) can be added as keys to WeakMap (eg. myVehicles) object. When non object variables (eg. a string, a number) are used as keys, WeakMap throws TypeError - Invalid value (lines: 13-14). However, WeakMap values can be anything including a function (eg. make, line 16), an object (eg. myTrucks, line: 18).

WeakMap Properties & Methods

The WeakMap collections have only the following properties & methods:

Unlike in Map, the WeakMap does not support keys(), values() and entries() methods and therefore these values can not be accessed.

Garbage collection

In JS the term Garbage Collection refers to memory management. To quote from MDN ” JavaScript values are allocated when things (objects, strings, etc.) are created and “automatically” freed when they are not used anymore. The latter process is called garbage collection.

Note: Memory management in JavaScript and garbage collection is a more advanced topic and it will be discussed in a separate post.

Lets examine below a simple example of object assignment:

//initialize myCar Object
let myCar = { make: "Toyota" };
//access myCar
console.log(myCar);//OUTPUT => {make: "Toyota"}

//overwrite myCar value with null
myCar = null; 
//access myCar
console.log(myCar);//OUTPUT => null

In the above example, myCar is initialized as an object with one property make and Toyota as its value (line 2). The object can be accessed with myCar variable (line 4) because myCar works as reference to the object. If the value of myCar object variable is overwritten (line 7), the object will be removed from the memory (line 9).

Memory Management: Map vs WeakMap

Revisiting myCar example from above, lets define the object using Maps and weakMap.

//initialize a car object
let car = { make: "Toyota" };

//assign the object to myCar map
let myCar = new Map();
myCar.set(car, "a string value");
//access myCar map object
myCar; // OUTPUT => Map(1) {{…} => "a string value"}

//access myCar 
myCar.get(car); // => "a string value"
myCar.has(car); // => true

//re-assign object different value
car = null;
//access myCar map Object
console.log(myCar);// OUTPUT => Map(1) {{…} => "a string value"}
myCar.get(car);  //  => undefined
myCar.has(car); // => false
//access with map.key()
myCar.keys(car); // OUTPUT => MapIterator {{…}}
//access with size property
myCar.size;// OUTPUT => 1

In the example above, when value of a item (eg. car) is reassigned to null (line 15), the WeakMap.get(key) and WeakMap.has(key) methods confirmed (lines: 18-19) that the car item does not exist anymore. However, the car object is still stored inside the map object and it can be accessed by using map.keys(value) method as shown in line 21.

Lets examine what happens, if the same car object (line 2) is added to WeakMap object (below):

//initialize a car object
let car = { make: "Toyota" };

//assign the object to myCar WeakMap
let myCar = new WeakMap();
myCar.set(car, "a string value");
//access myCar
console.log(myCar); // OUTPUT =>WeakMap {{…} => "a string value"}
//access car object 
myCar.get(car); // OUTPUT => "a string value"
myCar.has(car); // => true

//overwrite car object reference
car = null;
//access myCar
myCar; //OUTPUT => WeakMap {{…} => "a string value"}
//access car
myCar.get(car); // OUTPUT => undefined
myCar.has(car); // OUTPUT => false

In the example above, when the value of car object was assigned to null (line 37), it is confirmed with get and has methods (lines: 41-42) that the car item does not exist. Because WeakMap does not support key() method and the car object is linked only through key of WeakMap, it is automatically deleted from memory.

WeakMap Use Case Example

The WeakMap object is commonly used to store private data or to hide implementation details. In the following example adopted from The Modern JavaScript Tutorial, vehicle mileage log is stored as a Map object. When the vehicle is not in service, its mileage data is not essential.

//define variables
let toyota = {make: "Toyota"};
//map toyota mileage
let toyotaMileage = new Map();
//add mileage with set
toyotaMileage.set(toyota, 25000); // =>Map(1) {{…} => 25000}

//vehicle not in service
toyota = null;
// still in memory
toyotaMileage.has(toyota); // => false
toyotaMileage.size; // => 1

In the example above, we would like to store mileage log of our Toyota as a toyotaMileage map object (line 4) using toyota, an object variable (line 2), as its key and mileage as its value (line 6). When the vehicle was out of service, its value was assigned to null (line 9). Although, Map.has(key) method shows its value is erased (line 11), it still can be accessed with Map.size property (line 12). It needs to be removed manually, and if there are hundreads of such records and scattered in different location, it approach becomes quite cumbersome. This problem can be overcome using WeakMap object, as shown below:

// map with WeakMap Constructor
let toyotaMileage = new WeakMap();
//add mileage with set
toyotaMileage.set(toyota, 25000); // =>WeakMap {{…} => 25000}
//vehicle not in service
toyota = null;

//access WeakMap memory with size
toyotaMileage.has(toyota); // => false
toyotaMileage.size; // => undefined

In the example above, when WeakMap object was used (line 14) instead of Map object to store mileage log (line 16), when the value of variable object (eg. toyota, line 2) was assigned to null (line 8), the object variable was automatically removed from its memory as well (line 22; compare with line 12).

WeakSet

The WeakSet objects are similar to Sets objects which are only weakly held in a collection. The WeakSet objects are not enumerable.

The MDN WeakSet Reference Document outlines following differences between WeakSet and Sets:

  • In contrast to Sets, WeakSets are collections of objects only and not of arbitrary values of any type.
  • The WeakSet is weak (objects are weakly held in the collections). If the stored objects are not referenced then they can be removed from memory (garbage collected).
  • Because there is no permanent list of current objects in collections (garbage collectable), the WeakSet are not enumerable.
WeakSet Properties & Methods

The WeakSet collections have only the following properties & methods:

Similar to WeakMap, the WeakSet also does not support keys(), values() and entries() methods and therefore these values can not be accessed.

Using weakSet Object

In the example below, we will look at basic operations with WeakSet object:

// initialize object variables
let cars = {}; //empty object 
let trucks = {}; // empty object

//initialize WeakSet object with constructor
let myVehicles = new WeakSet(); // an empty WeakSet

// add items to WeakSet
myVehicles.add(cars); // => WeakSet {{…}}
myVehicles.add(foo); // => ReferenceError: foo not defined
myVehicles.add(trucks); // => WeakSet {{…}, {…}} 

// check with has() method
myVehicles.has(cars); // => true
myVehicles.has(foo); // => ReferenceError: foo not defined
myVehicles.has(trucks); // => true

// remove item with delete()
myVehicles.delete(cars); // => true
myVehicles.has(cars);// => false, item removed

In the example above, two object variables (cars, trucks) were initialized (lines: 2-3).  Only object variables can be added (lines: 9, 11) to myVehicles WeakSet map object (line 6). When a non-object item is added (eg foo, line 10), it throws an ReferenceError : item not found (line 10). Because WeakSet has limited methods available, items can be checked with only WeakSet.has(key) (lines: 14-16, 20) and there is no other way to determine its size nor items can be enumerated.

WeakSet Use Case Example

The WeakSet is used to tag objects without mutating them. The following example adapted from The Modern JavaScript Tutorial demonstrates use of WeakSet to remove records from its memory after the record is deleted.

//initialize variables
let toyota = {make: "Toyota"};
let ford = {make: "Ford"};
let honda = {make: "Honda"};

//map vehicles wih WeakSet object
let vehicles = new WeakSet();

//add the vehicle object to WeakSet
vehicles.add(toyota); // => WeakSet {{…}}
vehicles.add(ford); // => WeakSet {{…}, {…}}
vehicles.add(honda); // => WeakSet {{…}, {…}, {…}}

//check Ford with has
vehicles.has(ford); // => true
//delete ford with delete method
vehicles.delete(ford); // => true

//verify for object is deleted
vehicles.has(ford); // => false

Unlike in the Sets, WeakSet can be used to permanently remove a record from its memory after the record (a variable, or an array) is deleted. In the example above, when ford vehicle was deleted (line 17), it was automatically removed from its memory too (line 21). There is no need to manually clean it up from its memory. One of the limitations of this method is that number of remaining items and list of items can’t not be determined.

Wrapping Up

In previous post, Learning JavaScript Maps & Sets, basic usages and example of their use cases are discussed. In this post, we looked at WeakMaps and WeakSets objects their basic usage, use case examples and their limitations. Additional information can be found in the MDN Documentation.

Related Post: Learning JavaScript Maps & Sets

Useful Resources & Links

While preparing this post, I have referred the following references extensively. Visit original link for additional information.