WeakMap you do n’t know

Posted May 27, 202013 min read

I believe that many readers are no stranger to the Map introduced by ES6, and some of them may have heard of WeakMap. Both Health Map How Health WeakMap? With this question, this article will introduce the relevant knowledge of WeakMap in detail around the following aspects.

you-dont-know-weakmap.jpg

Created a WeChat group that "re-learns TypeScript", want to add group friends, add me WeChat "semlinker", pay attention to learning TS There are currently 38 TS topics published.

1. What is garbage collection

In computer science, Garbage Collection(abbreviated as GC) refers to an automatic memory management mechanism. When a part of the memory space occupied by a program is no longer accessed by the program, the program will use the garbage collection algorithm to return this part of the memory space to the operating system. The garbage collector can reduce the burden on programmers and also reduce errors in the program.

Garbage collection originated from the LISP language, and it has two basic principles:

  • Consider that an object will not be accessed in the future program operation;
  • Reclaim the memory occupied by these objects.

JavaScript has an automatic garbage collection mechanism. The principle of this garbage collection mechanism is actually very simple:find those variables that are no longer used and then release the memory they occupy. The garbage collector will periodically execute this at fixed intervals. operating.

gc-cycle.jpg

(Source:Garbage Collection:V8 ’s Orinoco)

Local variables only exist during the execution of the function. In this process, the local variables are generally allocated space on the stack memory, and then these variables are used in the function until the end of the function execution. The garbage collector must track the usage of each variable, and mark those variables that are no longer in use, to be able to reclaim the memory they occupy in time in the future. The strategies for identifying useless variables mainly include reference counting and mark removal.

1.1 Reference counting

The earliest and simplest method of garbage collection is to add a counter to an object that occupies physical space. The counter is incremented when there are other objects referencing the object, otherwise it is decremented when the reference is lifted. This algorithm will periodically check the counters of objects that have not been reclaimed. If it is zero, the physical space occupied by it will be reclaimed because the objects at this time are no longer accessible.

Reference counting is relatively simple to implement, but it cannot recycle circularly referenced storage objects, such as:

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; //o1 refers to o2
  o2.p = o1; //o2 refers to o1
}

f();

In order to solve this problem, the garbage collector introduces the mark removal method.

1.2 Mark removal method

The mark removal method mainly divides the GC's garbage collection process into a mark phase and a clear phase:

  • Marking stage:mark all active objects;
  • Clearing stage:destroy the unmarked(that is, inactive objects).

The most commonly used garbage collection method in JavaScript is mark-and-sweep. When a variable enters the environment, the variable is marked as "entering the environment", and when the variable leaves the environment, it is marked as "leaving the environment" .

The specific garbage collection process of the mark removal method is shown in the figure below:

gc_mark_sweep.gif

(Source:How JavaScript works:memory management + how to handle 4 common memory leaks)

In daily work, for objects that are no longer used, we usually want them to be collected by the garbage collector. At this time, you can use null to override the reference of the corresponding object, for example:

let sem = {name:"Semlinker"};
//The object can be accessed, sem is its reference
sem = null; //Overwrite reference
//The object will be cleared from memory

However, when data structures such as objects and arrays are in memory, their child elements, such as the properties of objects and elements of arrays, are accessible. For example, if you put an object into an array, as long as the array exists, the object will exist even if there is no other reference to the object. such as:

let sem = {name:"Semlinker"};
let array = [sem];
sem = null; //Overwrite reference

//sem is stored in an array, so it will not be collected by the garbage collection mechanism
//We can get it by array [0]

Similarly, if we use an object as the key of a regular Map, then when Map exists, the object will also exist. It will take up memory and will not be recycled by the garbage collection mechanism. such as:

let sem = {name:"Semlinker"};

let map = new Map();
map.set(sem, "Full Stack of Immortal Road");
sem = null; //Overwrite reference

//sem is stored in the map
//We can use map.keys() to get it

So how to solve the garbage collection problem of the above Map? At this time we need to understand WeakMap.

2. Why WeakMap

2.1 Difference between Map and WeakMap

I believe that many readers are no stranger to Map in ES6, already have Map, why there is WeakMap, what is the difference between them? The main differences between Map and WeakMap:

  • The key of the Map object can be any type, but the key in the WeakMap object can only be an object reference;
  • WeakMap cannot contain unreferenced objects, otherwise it will be automatically cleared out of the collection(garbage collection mechanism);
  • WeakMap objects are not enumerable, and the size of the collection cannot be obtained.

In JavaScript, the Map API can be implemented by making its four API methods share two arrays(one for storing keys and one for storing values). When setting a value for this Map, both the key and the value will be added to the end of the two arrays. Thus, the indexes of keys and values ​​correspond in the two arrays. When fetching values ​​from this Map, you need to traverse all the keys, and then use the index to retrieve the corresponding value from the array of stored values.

But such an implementation will have two big disadvantages. First, the assignment and search operations are O(n) time complexity(n is the number of key-value pairs), because both operations need to traverse the entire array To match. Another disadvantage is that it may cause a memory leak, because the array will always refer to each key and value. This kind of reference makes the garbage collection algorithm unable to recycle them, even if no other references exist.

In contrast, the native WeakMap holds "weak references" to each key object, which means that garbage collection can be performed correctly when no other references exist. The structure of the native WeakMap is special and effective. The key used for mapping is only valid when it is not recycled.

Because of such weak references, the key of WeakMap is not enumerable(there is no way to give all keys). If the key is enumerable, its list will be affected by the garbage collection mechanism, thereby obtaining uncertain results. Therefore, if you want a list of key values ​​for this type of object, you should use Map . And if you want to add data to the object, and do not want to interfere with the garbage collection mechanism, you can use WeakMap.

So for the garbage collection problem we encountered earlier, we can use WeakMap to solve it, as follows:

let sem = {name:"Semlinker"};

let map = new WeakMap();
map.set(sem, "Full Stack of Immortal Road");
sem = null; //Overwrite reference

2.2 WeakMap and garbage collection

Is WeakMap really as amazing as the introduction? Let ’s test the impact of Map and WeakMap on garbage collection in the same scenario. First we create two files:map.js and weakmap.js.

map.js

//map.js
function usageSize() {
  const used = process.memoryUsage(). heapUsed;
  return Math.round((used/1024/1024) * 100)/100 + "M";
}

global.gc();
console.log(usageSize()); //≈ 3.19M

let arr = new Array(10 * 1024 * 1024);
const map = new Map();

map.set(arr, 1);
global.gc();
console.log(usageSize()); //≈ 83.19M

arr = null;
global.gc();
console.log(usageSize()); //≈ 83.2M

After creating map.js, enter the node --expose-gc map.js command on the command line to execute the code in map.js, where the --expose-gc parameter indicates that the garbage collection mechanism is allowed to be executed manually.

weakmap.js

function usageSize() {
  const used = process.memoryUsage(). heapUsed;
  return Math.round((used/1024/1024) * 100)/100 + "M";
}

global.gc();
console.log(usageSize()); //≈ 3.19M

let arr = new Array(10 * 1024 * 1024);
const map = new WeakMap();

map.set(arr, 1);
global.gc();
console.log(usageSize()); //≈ 83.2M

arr = null;
global.gc();
console.log(usageSize()); //≈ 3.2M

Similarly, after creating weakmap.js, enter the node --expose-gc weakmap.js command on the command line to execute the code in weakmap.js. By comparing the output of map.js and weakmap.js, we know that after arr defined in weakmap.js is cleared, the heap memory it occupies is successfully recovered by the garbage collector.

Let's take a brief analysis of the main reasons for the above differences:

For map.js, since both arr and Map retain a strong reference to the array, simply clearing the arr variable memory in the Map has not been released because Map still has a reference count. In WeakMap, its keys are weak references and are not counted in the reference count, so when arr is cleared, the array will be cleared by garbage collection because the reference count is 0.

After understanding the above, let's introduce WeakMap formally.

3. Introduction to WeakMap

The WeakMap object is a collection of key/value pairs, where the keys are weakly referenced. WeakMap key can only be of type Object. The original data type cannot be used as a key(such as Symbol).

3.1 Syntax

new WeakMap([iterable])

iterable:an array(binary array) or other iterable object whose elements are key-value pairs. Each key-value pair will be added to the new WeakMap. null will be treated as undefined.

3.2 Properties

  • length:the value of the attribute is 0;
  • prototype:the prototype of the WeakMap constructor. Allows adding attributes to all WeakMap objects.

3.3 Methods

  • WeakMap.prototype.delete(key):remove key related objects. After execution, WeakMap.prototype.has(key) returns false.
  • WeakMap.prototype.get(key):return key related object, or undefined(when there is no key related object).
  • WeakMap.prototype.has(key):returns a boolean according to whether there is a key related object.
  • WeakMap.prototype.set(key, value):Set a set of key related objects in WeakMap and return this WeakMap object.

3.4 Examples

const wm1 = new WeakMap(),
      wm2 = new WeakMap(),
      wm3 = new WeakMap();
const o1 = {},
      o2 = function() {},
      o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); //value can be any value, including an object or a function
wm2.set(o3, undefined);
wm2.set(wm1, wm2); //Key and value can be any object, even another WeakMap object

wm1.get(o2); //"azerty"
wm2.get(o2); //undefined, there is no o2 key in wm2
wm2.get(o3); //undefined, the value is undefined

wm1.has(o2); //true
wm2.has(o2); //false
wm2.has(o3); //true(even if the value is undefined)

wm3.set(o1, 37);
wm3.get(o1); //37

wm1.has(o1); //true
wm1.delete(o1);
wm1.has(o1); //false

After introducing the basic knowledge of WeakMap, let's introduce the application of WeakMap.

四 、 WeakMap application

4.1 Cache calculation results via WeakMap

With WeakMap, you can associate the results of previous calculations with objects without worrying about memory management. The following function countOwnKeys() is an example:it caches previous results in WeakMap cache.

const cache = new WeakMap();

function countOwnKeys(obj) {
  if(cache.has(obj)) {
    return [cache.get(obj), 'cached'];
  } else {
    const count = Object.keys(obj) .length;
    cache.set(obj, count);
    return [count, 'computed'];
  }
}

After creating the countOwnKeys method, let ’s test it in detail:

let obj = {name:"kakuqo", age:30};
console.log(countOwnKeys(obj));
//[2, 'computed']
console.log(countOwnKeys(obj));
//[2, 'cached']
obj = null; //set to null when the object is not in use

4.2 Keep private data in WeakMap

In the following code, WeakMap _counter and _action are used to store the values ​​of the virtual attributes of the following instances:

const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }

  dec() {
    let counter = _counter.get(this);
    counter--;
    _counter.set(this, counter);
    if(counter === 0) {
      _action.get(this)();
    }
  }
}

After creating the Countdown class, let ’s test it in detail:

let invoked = false;

const countDown = new Countdown(3,() => invoked = true);
countDown.dec();
countDown.dec();
countDown.dec();

console.log(`invoked status:${invoked}`)

When it comes to private properties of classes, we must not mention ECMAScript Private Fields .

Fifth, ECMAScript private fields

5.1 Introduction to ES Private Fields

Before introducing the private fields of ECMAScript, let's witness its "Fang Rong":

class Counter extends HTMLElement {
  #x = 0;

  clicked() {
    this. # x ++;
    window.requestAnimationFrame(this.render.bind(this));
  }

  constructor() {
    super();
    this.onclick = this.clicked.bind(this);
  }

  connectedCallback() {this.render();}

  render() {
    this.textContent = this. # x.toString();
  }
}

window.customElements.define('num-counter', Counter);

At first glance, it seems awkward to see # x. At present, the TC39 committee has reached an agreement on this, and the proposal has entered Stage 3. So why use # symbols instead of other symbols?

The TC39 committee explained that they also made a deliberate decision and finally chose the # symbol instead of using the private keyword. It also discusses the use of private and # symbols together. And also intends to reserve a @ keyword as a protected attribute.

From Mito greatly:Why use # sign for private property of JavaScript

https://zhuanlan.zhihu.com/p/...

The TypeScript 3.8 version has started to support ECMAScript private fields, as follows:

class Person {
  #name:string;

  constructor(name:string) {
    this. # name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this. # name}!`);
  }
}

let semlinker = new Person("Semlinker");

semlinker. # name;
//~~~~~
//Property '#name' is not accessible outside class 'Person'
//because it has a private identifier.

Unlike regular attributes(even attributes declared with the private modifier), private fields must bear the following rules in mind:

  • Private fields start with # characters, sometimes we call them private names;
  • Each private field name is uniquely limited to the class it contains;
  • You cannot use TypeScript accessibility modifiers(such as public or private) on private fields;
  • Private fields cannot be accessed outside the included class, and cannot even be detected.

Speaking of this, what is the difference between a private field defined with # and a field defined with the private modifier? Now let's first look at an example of private:

class Person {
  constructor(private name:string) {}
}

let person = new Person("Semlinker");
console.log(person.name);

In the above code, we create a Person class, which uses the private modifier to define a private property name, then use this class to create a person object, and then access it through person.name The private property of the person object, then the TypeScript compiler will prompt the following exception:

Property 'name' is private and only accessible within class 'Person'.(2341)

How to solve this anomaly? Of course you can use type assertions to convert person to any type:

console.log((person as any) .name);

Although the exception prompt of the TypeScript compiler is resolved in this way, we can still access the private properties inside the Person class at runtime. Why is this happening? Let's take a look at the ES5 code generated by compilation, maybe you will know the answer:

var Person =/** @class */(function() {
    function Person(name) {
      this.name = name;
    }
    return Person;
}());

var person = new Person("Semlinker");
console.log(person.name);

At this time, I believe that some friends will be curious about what code will be generated after compiling the private field defined by the ## in TypeScript 3.8 or later:

class Person {
  #name:string;

  constructor(name:string) {
    this. # name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this. # name}!`);
  }
}

The above code target is set to ES2015, which will compile and generate the following code:

"use strict";
var __classPrivateFieldSet =(this && this .__ classPrivateFieldSet)
  || function(receiver, privateMap, value) {
    if(! privateMap.has(receiver)) {
      throw new TypeError("attempted to set private field on non-instance");
    }
    privateMap.set(receiver, value);
    return value;
};

var __classPrivateFieldGet =(this && this .__ classPrivateFieldGet)
  || function(receiver, privateMap) {
    if(! privateMap.has(receiver)) {
      throw new TypeError("attempted to get private field on non-instance");
    }
    return privateMap.get(receiver);
};

var _name;
class Person {
    constructor(name) {
      _name.set(this, void 0);
      __classPrivateFieldSet(this, _name, name);
    }
    greet() {
      console.log(`Hello, my name is ${__ classPrivateFieldGet(this, _name)}!`);
    }
}
_name = new WeakMap();

By observing the above code, ECMAScript private fields defined with # will be stored via WeakMap objects, and the compiler will generate two methods __classPrivateFieldSet and __classPrivateFieldGet for setting and getting values. After introducing the related content of private fields in a single class, let's take a look at the performance of private fields in the case of inheritance.

5.2 ES private field inheritance

In order to compare the difference between regular fields and private fields, let's first look at the performance of regular fields in inheritance:

class C {
  foo = 10;

  cHelper() {
    return this.foo;
  }
}

class D extends C {
  foo = 20;

  dHelper() {
    return this.foo;
  }
}

let instance = new D();
//'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); //prints '20'
console.log(instance.dHelper()); //prints '20'

Obviously, whether it is to call the cHelper() method defined in the subclass or the dHelper() method defined in the parent class, it will eventually output the foo attribute on the subclass. Next we look at the performance of private fields in inheritance:

class C {
  #foo = 10;

  cHelper() {
    return this. # foo;
  }
}

class D extends C {
  #foo = 20;

  dHelper() {
    return this. # foo;
  }
}

let instance = new D();
//'this. # foo' refers to a different field within each class.
console.log(instance.cHelper()); //prints '10'
console.log(instance.dHelper()); //prints '20'

By observing the above results, we can know that this. # Foo in thecHelper()method anddHelper()method points to different fields in each class. Regarding the other contents of the private fields of ECMAScript, we will not expand it anymore. Interested readers can read the relevant materials by themselves.

VI. Summary

This article mainly introduces the role and application scenarios of WeakMap in JavaScript. In fact, in addition to WeakMap, there is also a WeakSet. As long as the object is added to WeakMap or WeakSet, GC can recycle its occupied memory when the condition is triggered.

But in fact, JavaScript's WeakMap is not a weak reference in the true sense:in fact, as long as the key is still alive, it strongly references its content. WeakMap only weakly references its contents after the key is garbage collected. In order to provide a real weak reference, TC39 proposed the WeakRefs proposal.

WeakRef is a more advanced API that provides true weak references and inserts a window in the life cycle of the object. It can also solve the problem that WeakMap only supports the object type as Key.

7. Reference Resources

My full stack Xiuxian Road subscription number, I will regularly share Angular, TypeScript, Node.js related articles, welcome interested partners to subscribe!

learned-ts.jpg