Set and Map Data structure

Set

Basic usage

ES6 provides a new data structure Set. It is similar to an array, but the values ​​of the members are unique and there are no duplicate values.

Set itself is a constructor, used to generate Set data structure.

const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach((x) => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4

The above code adds members to the Set structure through the add() method, and the result shows that the Set structure will not add duplicate values.

The Set function can accept an array (or other data structure with iterable interface) as a parameter for initialization.

// Example 1
const set = new Set([1, 2, 3, 4, 4]);
[...set];
// [1, 2, 3, 4]

// Example 2
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size; // 5

// example three
const set = new Set(document.querySelectorAll("div"));
set.size; // 56

// similar to
const set = new Set();
document.querySelectorAll("div").forEach((div) => set.add(div));
set.size; // 56

In the above code, both examples 1 and 2 are that the Set function accepts an array as a parameter, and example 3 accepts an array-like object as a parameter.

The above code also shows a way to remove duplicate members of an array.

// Remove duplicate members of the array
[...new Set(array)];

The above method can also be used to remove repeated characters in a string.

[...new Set("ababbc")].join("");
// "abc"

When adding a value to Set, no type conversion occurs, so 5 and "5" are two different values. Set internally determines whether two values ​​are different. The algorithm used is called "Same-value-zero equality", which is similar to the exact equality operator (===). The main difference is that it considers NaN when adding values ​​to Set. Equal to itself, and the exact equality operator considers NaN not equal to itself.

let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set; // Set {NaN}

The above code adds NaN twice to the Set instance, but only one is added. This shows that inside the Set, two NaNs are equal.

In addition, two objects are always not equal.

let set = new Set();

set.add({});
set.size; // 1

set.add({});
set.size; // 2

The above code means that since two empty objects are not equal, they are treated as two values.

Set instance properties and methods

The instance of the Set structure has the following properties.

-Set.prototype.constructor: Constructor, the default is Set function. -Set.prototype.size: Returns the total number of members of the Set instance.

Set instance methods are divided into two categories: operation methods (used to manipulate data) and traversal methods (used to traverse members). The following four operation methods are introduced first.

-Set.prototype.add(value): Add a value and return the Set structure itself. -Set.prototype.delete(value): delete a value and return a boolean value indicating whether the deletion was successful. -Set.prototype.has(value): Returns a boolean value indicating whether the value is a member of Set. -Set.prototype.clear(): Clear all members, no return value.

Examples of the above attributes and methods are as follows.

s.add(1).add(2).add(2);
// Note 2 is added twice

s.size; // 2

s.has(1); // true
s.has(2); // true
s.has(3); // false

s.delete(2);
s.has(2); // false

The following is a comparison to see whether the Object structure and the Set structure are written differently in determining whether to include a key.

// How to write the object
const properties = {
  width: 1,
  height: 1,
};

if (properties[someName]) {
  // do something
}

// How to write Set
const properties = new Set();

properties.add("width");
properties.add("height");

if (properties.has(someName)) {
  // do something
}

The Array.from method can convert the Set structure into an array.

const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);

This provides another way to remove duplicate members of the array.

function dedupe(array) {
  return Array.from(new Set(array));
}

dedupe([1, 1, 2, 3]); // [1, 2, 3]

Traverse operation

The instance of the Set structure has four traversal methods that can be used to traverse members.

-Set.prototype.keys(): Iterator that returns the key name -Set.prototype.values(): Iterator that returns key values -Set.prototype.entries(): Iterator that returns key-value pairs -Set.prototype.forEach(): Use the callback function to traverse each member

It needs to be pointed out that the traversal order of Set is the insertion order. This feature is sometimes very useful, such as using Set to save a list of callback functions, which can be guaranteed to be called in the order of addition.

(1) keys(), values(), entries()

The keys method, values method, and entries method return all iterator objects (see the chapter "Iterator Object" for details). Since the Set structure has no key names, only key values ​​(in other words, the key name and the key value are the same value), the behavior of the keys method and the values method are exactly the same.

let set = new Set(["red", "green", "blue"]);

for (let item of set.keys()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
  console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

In the above code, the iterator returned by the entries method includes both the key name and the key value, so each time an array is output, its two members are exactly equal.

The instance of the Set structure can be traversed by default, and its default traverser generating function is its values method.

Set.prototype[Symbol.iterator] === Set.prototype.values;
// true

This means that you can omit the values method and directly use the for...of loop to traverse the Set.

let set = new Set(["red", "green", "blue"]);

for (let x of set) {
  console.log(x);
}
// red
// green
// blue

(2)forEach()

Instances of the Set structure, like arrays, also have the forEach method, which is used to perform certain operations on each member without a return value.

let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ":" + value));
// 1: 1
// 4: 4
// 9: 9

The above code shows that the parameter of the forEach method is a processing function. The parameters of this function are consistent with the forEach of the array, which are the key value, the key name, and the set itself (the parameter is omitted in the above example). It should be noted here that the key name of the Set structure is the key value (the two are the same value), so the value of the first parameter and the second parameter are always the same.

In addition, the forEach method can also have a second parameter, which represents the this object inside the binding processing function.

(3) Application of traversal

The spread operator (...) uses the for...of loop internally, so it can also be used in the Set structure.

let set = new Set(["red", "green", "blue"]);
let arr = [...set];
// ['red','green','blue']

The combination of the spread operator and the Set structure can remove duplicate members of the array.

let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]

Moreover, the map and filter methods of arrays can also be used indirectly for Set.

let set = new Set([1, 2, 3]);
set = new Set([...set].map((x) => x * 2));
// Return Set structure: {2, 4, 6}

let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter((x) => x % 2 == 0));
// Return Set structure: {2, 4}

Therefore, using Set can easily implement Union, Intersect and Difference.

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// Union
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// intersection
let intersect = new Set([...a].filter((x) => b.has(x)));
// set {2, 3}

// (a relative to b) difference set
let difference = new Set([...a].filter((x) => !b.has(x)));
// Set {1}

If you want to change the original Set structure synchronously during the traversal operation, there is currently no direct method, but there are two workarounds. One is to use the original Set structure to map out a new structure, and then assign it to the original Set structure; the other is to use the Array.from method.

// method one
let set = new Set([1, 2, 3]);
set = new Set([...set].map((val) => val * 2));
// set value is 2, 4, 6

// Method Two
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, (val) => val * 2));
// set value is 2, 4, 6

The above code provides two methods to directly change the original Set structure during the traversal operation.

WeakSet

Meaning

The WeakSet structure is similar to Set, and it is also a collection of unique values. However, there are two differences between it and Set.

First of all, the members of WeakSet can only be objects, not other types of values.

const ws = new WeakSet();
ws.add(1);
// TypeError: Invalid value used in weak set
ws.add(Symbol());
// TypeError: invalid value used in weak set

The above code attempts to add a value and a Symbol value to the WeakSet, but an error is reported because the WeakSet can only place objects.

Secondly, the objects in WeakSet are all weak references, that is, the garbage collection mechanism does not consider WeakSet's references to the object, that is, if other objects no longer reference the object, the garbage collection mechanism will automatically reclaim the object occupied Memory, regardless of whether the object still exists in the WeakSet.

This is because the garbage collection mechanism judges the collection based on the reachability of the object. If the object can still be accessed, the garbage collection mechanism will not release this memory. After you finish using the value, you sometimes forget to dereference, causing the memory to not be released, which may cause a memory leak. The references in WeakSet are not included in the garbage collection mechanism, so this problem does not exist. Therefore, WeakSet is suitable for temporarily storing a group of objects and storing information bound to the objects. As long as these objects disappear from the outside, their references in the WeakSet will automatically disappear.

Due to the above characteristics, the members of WeakSet are not suitable for reference, because it will disappear at any time. In addition, since the number of members in the WeakSet depends on whether the garbage collection mechanism is running, the number of members is likely to be different before and after the operation, and when the garbage collection mechanism runs is unpredictable, ES6 stipulates that the WeakSet cannot be traversed.

These characteristics also apply to the WeakMap structure described later in this chapter.

Syntax

WeakSet is a constructor. You can use the new command to create a WeakSet data structure.

const ws = new WeakSet();

As a constructor, WeakSet can accept an array or array-like object as a parameter. (Actually, any object with an Iterable interface can be used as a parameter of WeakSet.) All members of the array will automatically become members of the WeakSet instance object.

const a = [
  [1, 2],
  [3, 4],
];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}

In the above code, a is an array, it has two members, both are arrays. Use a as the parameter of the WeakSet constructor, and the members of a will automatically become members of WeakSet.

Note that the members of the a array become members of the WeakSet, not the a array itself. This means that the members of the array can only be objects.

const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)

In the above code, the members of the array b are not objects, and an error will be reported if they are added to WeakSet.

The WeakSet structure has the following three methods.

-WeakSet.prototype.add(value): Add a new member to the WeakSet instance. -WeakSet.prototype.delete(value): Clear the specified member of the WeakSet instance. -WeakSet.prototype.has(value): Returns a Boolean value, indicating whether a value is in the WeakSet instance.

Below is an example.

const ws = new WeakSet();
const obj = {};
const foo = {};

ws.add(window);
ws.add(obj);

ws.has(window); // true
ws.has(foo); // false

ws.delete(window);
ws.has(window); // false

WeakSet has no size property and there is no way to traverse its members.

ws.size; // undefined
ws.forEach; // undefined

ws.forEach(function (item) {
  console.log("WeakSet has" + item);
});
// TypeError: undefined is not a function

The above code tried to get the size and forEach properties, but the results were unsuccessful.

WeakSet cannot be traversed because the members are weak references and may disappear at any time. The traversal mechanism cannot guarantee the existence of the members. It is very likely that the members cannot be retrieved just after the traversal is over. One use of WeakSet is to store DOM nodes without worrying about memory leaks when these nodes are removed from the document.

Below is another example of WeakSet.

const foos = new WeakSet();
class Foo {
  constructor() {
    foos.add(this);
  }
  method() {
    if (!foos.has(this)) {
      throw new TypeError(
        "Foo.prototype.method can only be called on an instance of Foo!"
      );
    }
  }
}

The above code guarantees that the instance method of Foo can only be called on the instance of Foo. The advantage of using WeakSet here is that the reference of foos to the instance will not be counted in the memory recovery mechanism, so when deleting the instance, there is no need to consider foos, and there will be no memory leaks.

Map

Meaning and basic usage

JavaScript objects are essentially a collection of key-value pairs (Hash structure), but traditionally they can only use strings as keys. This puts a lot of restrictions on its use.

const data = {};
const element = document.getElementById("myDiv");

data[element] = "metadata";
data["[object HTMLDivElement]"]; // "metadata"

The original intention of the above code is to use a DOM node as the key of the object data, but since the object only accepts a string as the key name, the element is automatically converted to the string [object HTMLDivElement].

To solve this problem, ES6 provides the Map data structure. It is similar to an object and is also a collection of key-value pairs, but the scope of "keys" is not limited to strings. Various types of values ​​(including objects) can be used as keys. In other words, the Object structure provides a "string-value" correspondence, and the Map structure provides a "value-value" correspondence, which is a more complete implementation of the Hash structure. If you need a "key-value pair" data structure, Map is more suitable than Object.

const m = new Map();
const o = { p: "Hello World" };

m.set(o, "content");
m.get(o); // "content"

m.has(o); // true
m.delete(o); // true
m.has(o); // false

The above code uses the set method of the Map structure to treat the object o as a key of m, then uses the get method to read the key, and then uses the delete method to delete the key.

The above example shows how to add members to the Map. As a constructor, Map can also accept an array as a parameter. The members of this array are an array of key-value pairs.

const map = new Map([
  ["name", "Zhang San"],
  ["title", "Author"],
]);

map.size; // 2
map.has("name"); // true
map.get("name"); // "Zhang San"
map.has("title"); // true
map.get("title"); // "Author"

The above code specifies two keys name and title when creating a new Map instance.

The Map constructor accepts an array as a parameter, and actually executes the following algorithm.

const items = [
  ["name", "Zhang San"],
  ["title", "Author"],
];

const map = new Map();

items.forEach(([key, value]) => map.set(key, value));

In fact, not only arrays, but any data structure that has an Iterator interface and each member is a two-element array (see the "Iterator" chapter for details) can be used as a parameter of the Map constructor. This means that both Set and Map can be used to generate a new Map.

const set = new Set([
  ["foo", 1],
  ["bar", 2],
]);
const m1 = new Map(set);
m1.get("foo"); // 1

const m2 = new Map([["baz", 3]]);
const m3 = new Map(m2);
m3.get("baz"); // 3

In the above code, we use the Set object and the Map object respectively as the parameters of the Map constructor, and as a result, a new Map object is generated.

If you assign a value to the same key multiple times, the following value will overwrite the previous value.

const map = new Map();

map.set(1, "aaa").set(1, "bbb");

map.get(1); // "bbb"

The above code assigns a value to the key 1 twice in a row, and the last value overwrites the previous value.

If an unknown key is read, undefined is returned.

new Map().get("asfddfsasadf");
// undefined

Note that only references to the same object, the Map structure treats it as the same key. Be very careful about this.

const map = new Map();

map.set(["a"], 555);
map.get(["a"]); // undefined

The set and get methods of the above code, on the surface, are for the same key, but in fact these are two different array instances with different memory addresses, so the get method cannot read the key and returns undefined.

Similarly, two instances of the same value are treated as two keys in the Map structure.

const map = new Map();

const k1 = ["a"];
const k2 = ["a"];

map.set(k1, 111).set(k2, 222);

map.get(k1); // 111
map.get(k2); // 222

In the above code, the values ​​of the variables k1 and k2 are the same, but they are treated as two keys in the Map structure.

It can be seen from the above that the key of the Map is actually bound to the memory address, as long as the memory address is different, it is regarded as two keys. This solves the problem of clash of attributes with the same name. When we expand other people’s libraries, if we use objects as key names, we don’t have to worry about our attributes with the same names as those of the original author.

If the key of Map is a simple type value (number, string, boolean), as long as the two values ​​are strictly equal, Map will treat it as a key. For example, 0 and -0 are a key, boolean The value true and the string true are two different keys. In addition, undefined and null are also two different keys. Although NaN is not strictly equal to itself, Map treats it as the same key.

let map = new Map();

map.set(-0, 123);
map.get(+0); // 123

map.set(true, 1);
map.set("true", 2);
map.get(true); // 1

map.set(undefined, 3);
map.set(null, 4);
map.get(undefined); // 3

map.set(NaN, 123);
map.get(NaN); // 123

Instance attributes and operation methods

The instance of the Map structure has the following properties and operation methods.

(1) size attribute

The size attribute returns the total number of members of the Map structure.

const map = new Map();
map.set("foo", true);
map.set("bar", false);

map.size; // 2

(2) Map.prototype.set(key, value)

The set method sets the key value corresponding to the key name key to value, and then returns the entire Map structure. If key already has a value, the key value will be updated, otherwise the key will be newly generated.

const m = new Map();

m.set("edition", 6); // key is a string
m.set(262, "standard"); // key is numeric
m.set(undefined, "nah"); // key is undefined

The set method returns the current Map object, so chain writing can be used.

let map = new Map().set(1, "a").set(2, "b").set(3, "c");

(3) Map.prototype.get(key)

The get method reads the key value corresponding to key, and if no key is found, it returns undefined.

const m = new Map();

const hello = function () {
  console.log("hello");
};
m.set(hello, "Hello ES6!"); // The key is a function

m.get(hello); // Hello ES6!

(4) Map.prototype.has(key)

The has method returns a boolean value indicating whether a key is in the current Map object.

const m = new Map();

m.set("edition", 6);
m.set(262, "standard");
m.set(undefined, "nah");

m.has("edition"); // true
m.has("years"); // false
m.has(262); // true
m.has(undefined); // true

(5) Map.prototype.delete(key)

The delete method deletes a key and returns true. If the deletion fails, false is returned.

const m = new Map();
m.set(undefined, "nah");
m.has(undefined); // true

m.delete(undefined);
m.has(undefined); // false

(6) Map.prototype.clear()

The clear method clears all members, no return value.

let map = new Map();
map.set("foo", true);
map.set("bar", false);

map.size; // 2
map.clear();
map.size; // 0

Traversal method

The Map structure natively provides three traverser generating functions and one traversal method.

-Map.prototype.keys(): a iterator that returns the key name. -Map.prototype.values(): a iterator that returns key values. -Map.prototype.entries(): It returns the iterator of all members. -Map.prototype.forEach(): Traverse all members of Map.

It is important to note that the traversal order of the Map is the insertion order.

const map = new Map([
  ["F", "no"],
  ["T", "yes"],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// or
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// equivalent to using map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

The example at the end of the above code represents the default iterator interface (Symbol.iterator property) of the Map structure, which is the entries method.

map[Symbol.iterator] === map.entries;
// true

The quicker way to convert the Map structure to an array structure is to use the spread operator (...).

const map = new Map([
  [1,'one'],
  [2,'two'],
  [3,'three'],
]);

[...map.keys()]
// [1, 2, 3]

[...map.values()]
// ['one','two','three']

[...map.entries()]
// [[1,'one'], [2,'two'], [3,'three']]

[...map]
// [[1,'one'], [2,'two'], [3,'three']]

Combining the map method and filter method of the array can realize the traversal and filtering of the Map (the Map itself does not have the map and filter methods).

const map0 = new Map().set(1, "a").set(2, "b").set(3, "c");

const map1 = new Map([...map0].filter(([k, v]) => k < 3));
// Generate Map structure {1 =>'a', 2 =>'b'}

const map2 = new Map([...map0].map(([k, v]) => [k * 2, "_" + v]));
// Generate Map structure {2 =>'_a', 4 =>'_b', 6 =>'_c'}

In addition, Map also has a forEach method, similar to the forEach method of arrays, which can also be traversed.

map.forEach(function (value, key, map) {
  console.log("Key: %s, Value: %s", key, value);
});

The forEach method can also accept a second parameter, which is used to bind this.

const reporter = {
  report: function (key, value) {
    console.log("Key: %s, Value: %s", key, value);
  },
};

map.forEach(function (value, key, map) {
  this.report(key, value);
}, reporter);

In the above code, the this of the callback function of the forEach method points to the reporter.

Mutual conversion with other data structures

(1) Map to array

As mentioned earlier, the most convenient way to convert a Map to an array is to use the spread operator (...).

const myMap = new Map().set(true, 7).set({ foo: 3 }, ["abc"]);
[...myMap];
// [[true, 7 ], [{foo: 3 }, ['abc']]]

(2) Convert array to Map

Pass the array to the Map constructor and it can be converted to a Map.

new Map([
  [true, 7],
  [{ foo: 3 }, ["abc"]],
]);
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
//}

(3) Convert Map to Object

If all Map keys are strings, they can be converted into objects without loss.

function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k, v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

const myMap = new Map().set("yes", true).set("no", false);
strMapToObj(myMap);
// {yes: true, no: false}

If there is a non-string key name, then this key name will be converted into a string and then used as the object key name.

(4) The object is converted to Map

Objects can be converted to Map by Object.entries().

let obj = { a: 1, b: 2 };
let map = new Map(Object.entries(obj));

In addition, you can also implement a conversion function yourself.

function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}

objToStrMap({ yes: true, no: false });
// Map {"yes" => true, "no" => false}

(5) Convert Map to JSON

There are two cases when converting Map to JSON. In one case, the keys of Map are all strings, and you can choose to convert them to object JSON at this time.

function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set("yes", true).set("no", false);
strMapToJson(myMap);
//'{"yes":true,"no":false}'

Another case is that the key name of the Map has a non-string, and you can choose to convert it to an array of JSON.

function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}

let myMap = new Map().set(true, 7).set({ foo: 3 }, ["abc"]);
mapToArrayJson(myMap);
//'[[true,7],[{"foo":3},["abc"]]]'

(6) Convert JSON to Map

JSON is converted to Map. Normally, all key names are strings.

function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"yes": true, "no": false}');
// Map {'yes' => true,'no' => false}

However, there is a special case where the entire JSON is an array, and each array member itself is an array with two members. At this time, it can be converted to Map in one-to-one correspondence. This is often the reverse operation of Map to JSON array.

function jsonToMap(jsonStr) {
  return new Map(JSON.parse(jsonStr));
}

jsonToMap('[[true,7],[{"foo":3},["abc"]]]');
// Map {true => 7, Object {foo: 3} => ['abc']}

WeakMap

Meaning

The structure of WeakMap is similar to the structure of Map, and it is also a collection of key-value pairs.

// WeakMap can use the set method to add members
const wm1 = new WeakMap();
const key = { foo: 1 };
wm1.set(key, 2);
wm1.get(key); // 2

// WeakMap can also accept an array,
// as a parameter of the constructor
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([
  [k1, "foo"],
  [k2, "bar"],
]);
wm2.get(k2); // "bar"

There are two differences between WeakMap and Map.

First of all, WeakMap only accepts objects as keys (except null), and does not accept other types of values ​​as keys.

const map = new WeakMap();
map.set(1, 2);
// TypeError: 1 is not an object!
map.set(Symbol(), 2);
// TypeError: Invalid value used as weak map key
map.set(null, 2);
// TypeError: Invalid value used as weak map key

In the above code, if the values ​​1 and Symbol are used as the key names of WeakMap, an error will be reported.

Secondly, the object pointed to by the key name of WeakMap is not counted in the garbage collection mechanism.

The design purpose of WeakMap is that sometimes we want to store some data on an object, but this will form a reference to this object. Please see the example below.

const e1 = document.getElementById("foo");
const e2 = document.getElementById("bar");
const arr = [
  [e1, "foo element"],
  [e2, "bar element"],
];

In the above code, e1 and e2 are two objects. We add some text descriptions to these two objects through the arr array. This forms the reference of arr to e1 and e2.

Once these two objects are no longer needed, we must manually delete this reference, otherwise the garbage collection mechanism will not release the memory occupied by e1 and e2.

// When e1 and e2 are not needed
// The reference must be deleted manually
arr[0] = null;
arr[1] = null;

The above writing method is obviously very inconvenient. Once you forget to write, it will cause memory leaks.

WeakMap was born to solve this problem. The objects referenced by its key names are all weak references, that is, the garbage collection mechanism does not take this reference into consideration. Therefore, as long as other references of the referenced object are cleared, the garbage collection mechanism will release the memory occupied by the object. In other words, once it is no longer needed, the key name object and the corresponding key-value pair in WeakMap will automatically disappear, without manually deleting the reference.

Basically, if you want to add data to the object without interfering with the garbage collection mechanism, you can use WeakMap. A typical application scenario is that you can use the WeakMap structure to add data to the DOM elements of the web page. When the DOM element is cleared, the corresponding WeakMap record will be automatically removed.

const wm = new WeakMap();

const element = document.getElementById("example");

wm.set(element, "some information");
wm.get(element); // "some information"

In the above code, first create a WeakMap instance. Then, a DOM node is stored as a key name in the instance, and some additional information is used as a key value and stored in the WeakMap together. At this time, the reference to element in WeakMap is a weak reference and will not be counted in the garbage collection mechanism.

In other words, the above DOM node object except for the weak reference of WeakMap, once the reference to the object in other locations is eliminated, the memory occupied by the object will be released by the garbage collection mechanism. The key-value pair saved by WeakMap will also disappear automatically.

In short, the special occasion of WeakMap is that the objects corresponding to its keys may disappear in the future. The WeakMap structure helps prevent memory leaks.

Note that WeakMap only weakly refers to the key name, not the key value. The key value is still a normal reference.

const wm = new WeakMap();
let key = {};
let obj = { foo: 1 };

wm.set(key, obj);
obj = null;
wm.get(key);
// Object {foo: 1}

In the above code, the key value obj is a normal reference. Therefore, even if the reference of obj is eliminated outside WeakMap, the reference inside WeakMap still exists.

Syntax of WeakMap

There are two main differences between WeakMap and Map in API. One is that there is no traversal operation (that is, no keys(), values() and entries() methods), and no size attribute. Because there is no way to list all the key names, the existence of a key name is completely unpredictable, depending on whether the garbage collection mechanism is running. The key name can be obtained at this moment, and the garbage collection mechanism suddenly runs in the next moment, and the key name is gone. In order to prevent uncertainty, it is uniformly stipulated that the key name cannot be obtained. The second is that it cannot be cleared, that is, the clear method is not supported. Therefore, WeakMap has only four methods available: get(), set(), has(), delete().

const wm = new WeakMap();

// size, forEach, and clear methods do not exist
wm.size; // undefined
wm.forEach; // undefined
wm.clear; // undefined

Examples of WeakMap

The WeakMap example is difficult to demonstrate, because it is impossible to observe that the references in it will automatically disappear. At this point, all other references are removed, and there are no more references to the key name of WeakMap, which makes it impossible to verify whether the key name exists.

Teacher He Shijun hint, if the value pointed to by the reference occupies a lot of memory, you can use Node's process.memoryUsage method figure it out. Based on this idea, netizen vtxf added the following example.

First, open the Node command line.

$ node --expose-gc

In the above code, the --expose-gc parameter indicates that the garbage collection mechanism is allowed to be executed manually.

Then, execute the following code.

// Manually perform a garbage collection to ensure that the obtained memory usage status is accurate
> global.gc();
undefined

// View the initial state of memory usage, heapUsed is about 4M
> process.memoryUsage();
{rss: 21106688,
  heapTotal: 7376896,
  heapUsed: 4153936,
  external: 9059}

> let wm = new WeakMap();
undefined

// Create a new variable key, pointing to an array of 5*1024*1024
> let key = new Array(5 * 1024 * 1024);
undefined

// Set the key name of the WeakMap instance, which also points to the key array
// At this time, the key array is actually referenced twice,
// The variable key is referenced once, and the key name of WeakMap is referenced a second time
// However, WeakMap is a weak reference, for the engine, the reference count is still 1
> wm.set(key, 1);
WeakMap {}

> global.gc();
undefined

// At this time, the memory occupied by heapUsed has increased to 45M
> process.memoryUsage();
{rss: 67538944,
  heapTotal: 7376896,
  heapUsed: 45782816,
  external: 8945}

// Clear the reference of the variable key to the array,
// but did not manually clear the reference to the array by the key name of the WeakMap instance
> key = null;
null

// Perform garbage collection again
> global.gc();
undefined

// The memory usage of heapUsed changes back to about 4M,
// You can see that the key name reference of WeakMap does not prevent gc from recycling memory
> process.memoryUsage();
{rss: 20639744,
  heapTotal: 8425472,
  heapUsed: 3979792,
  external: 8956}

In the above code, as long as the external reference disappears, the internal reference of WeakMap will be automatically cleared by garbage collection. It can be seen that with the help of WeakMap, solving memory leaks will be much simpler.

The Memory panel of the Dev Tools of the Chrome browser has a trash can button to force garbage collection (garbage collect). This button can also be used to observe whether the references in the WeakMap disappear.

Use of WeakMap

As mentioned earlier, the typical application of WeakMap is the DOM node as the key name. Below is an example.

let myWeakmap = new WeakMap();

myWeakmap.set(document.getElementById("logo"), { timesClicked: 0 });

document.getElementById("logo").addEventListener(
  "click",
  function () {
    let logoData = myWeakmap.get(document.getElementById("logo"));
    logoData.timesClicked++;
  },
  false
);

In the above code, document.getElementById('logo') is a DOM node. Whenever a click event occurs, the status is updated. We put this state as the key value in the WeakMap, and the corresponding key name is the node object. Once the DOM node is deleted, the state will disappear automatically, and there is no risk of memory leaks.

Another use of WeakMap is to deploy private properties.

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);
    if (counter < 1) return;
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}

const c = new Countdown(2, () => console.log("DONE"));

c.dec();
c.dec();
// DONE

In the above code, the two internal attributes of the Countdown class, _counter and _action, are weak references to the instance, so if the instance is deleted, they will also disappear without causing a memory leak.