WeakMaps and WeakSets

Garbage collection

In JavaScript values in objects are not garbage collected when all references outside the object are lost. As long as the object is reachable, its properties and elements are reachable. This potentially causes a memory leak. This also hold for maps and sets.


let myKey = { a: 1 };
const myMap = new Map([
 [myKey, "some value"],
]);

myKey = {};

// All { a: 1 } references outside the map are lost.
// Yet, { a: 1 } is still reachable:
for (const [key, value] of myMap) {
  console.log(key);
}
// logs:
// { a: 1 }

In the example above, object { a: 1 } and the value associated with that key in the map will not be garbage collected. This is a memory leak. If nothing else in the program is referencing the key, the key and associated value should be eligible for garbage collection.

Weak referenced keys

The WeakMap and WeakSet objects closely mirror their non-weak counterparts Map and Set. Only now keys are weakly referenced, meaning that the mark-and-sweep algorithm won't consider keys as reachable if nothing else strongly refers to them. Now there is no memory leak.

Note that keys can only weakly reference an object. In JavaScript primitive values are not referenced, but (strongly) stored in a variable, or in this case in a key. In case of primitive valued keys, the weak referenced key removal would never trigger, making them stay in the collection forever.


let myKey = { a: 1 };
const myMap = new WeakMap([
 [myKey, "some value"],
]);

myKey = {};

// Now object { a: 1 } and the associated value are subject to garbage collection!

let myKey = "someKey";
const myMap = new WeakMap([
 [myKey, "some value"],
]); // TypeError: WeakMap key must be an object

WeakMap and WeakSet are not iterable

Garbage collection happens automatically. JavaScript does not provide a way to programmatically trigger garbage collection. It is not specified when unreachable values are removed from memory, it depends on how the algorithm is implemented in the JavaScript engine. This means that methods and operations that need to consider the number and order of elements in the collection cannot be reliable. Therefore WeakMap and WeakSet do not have a size method and iteration over the elements is not possible.

WeakMap only has the methods set(key, value), get(key), delete(key) and has(key). WeakSet only has the methods add(value), delete(value) and has(value).


let myKey = { a: 1 };
const myMap = new WeakMap([
 [myKey, "some value"],
 [[2], "..."], 
]);

for (const [key, value] of myMap) {
  console.log(key);
} // TypeError: myMap is not iterable

Note that get(key) and has(key) will not work when objects have no strong references anymore, regardless whether garbage collection occurred or not:


let myKey = { a: 1 };
const myMap = new Map([  // not a WeakMap, so no garbage collection
 [myKey, "some value"],
 [[2], "..."], 
]);

myKey = null;

console.log(myMap.get(myKey)); //logs: undefined // key null does not exist
console.log(myMap.get({ a: 1 })); //logs: undefined // { a: 1 } !== { a: 1 }

Examples

In the next example a function useObj() counts how many times it is called on a particular object and stores this count per object in a WeakMap.


const callCount = new WeakMap();
function useObj(obj){
  // do whatever with obj
  if (!callCount.has(obj)) {
    obj.status = "active";
  }
  // update callCount:
  let called = callCount.get(obj) || 0;
  called++; // called one more time
  callCount.set(obj, called);
}

// a temporary object is used a number of times throughout the program via useObj():
let tempObj = { name: "some temporary object" }; 
useObj(tempObj);
useObj(tempObj);
useObj(tempObj);

console.log(`Object "${tempObj.name}" was used ${callCount.get(tempObj)} times`); // logs: 'Object "some temporary object" was used 3 times'
// the object is not needed any more:
tempObj = null;
// Object { name: "some temporary object" } and the associated value are subject to garbage collection.

The above example is an adaptation of this Stackoverflow answer.

WeakSet has only the has() method to read the collection. This makes a WeakSet suitable for tracking the existence of temporary objects.


const mySet = new WeakSet();

let tempObj1 = {},
    tempObj2 = {},
	tempObj3 = ["..."],
	tempObj4 = {};

mySet.add(tempObj1);
mySet.add(tempObj2);
mySet.add(tempObj3);
mySet.add(tempObj4);

console.log(mySet.has(tempObj3)); // logs: true

tempObj3 = null;
console.log(mySet.has(tempObj3)); // logs: false

// Array ["..."] is now queued up for the trash can.