ES6 series XIII: Set and Map data structures

ES6 series XIII: Set and Map data structures

Set

Basic Usage

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

Set itself is a constructor used to generate a 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. The results show that the Set structure will not add duplicate values.

The Set function can accept an array (or other data structures 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 3
const set = new Set(document.querySelectorAll('div'));
set.size // 56

// be 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 Set functions that accept arrays as parameters, and example 3 accepts objects similar to arrays as parameters.

The above code also shows a method 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 duplicate characters in the 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 determines whether two values are different internally. The algorithm used is called "same value zero equality", which is similar to the exact equality operator (= = =). The main difference is that when adding values to set, NaN is considered equal to itself, while 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 to the Set instance twice, but only one is added. This shows that within the Set, the two NaN are equal.

In addition, two objects are always unequal.

let set = new Set();

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

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

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

Set instance properties and methods

An instance of the Set structure has the following properties.

  • Set.prototype.constructor: constructor, which is set function by default.
  • Set.prototype.size: returns the total number of members of the set instance.

The methods of Set instances are divided into two categories: operation methods (for manipulating data) and traversal methods (for traversing members). Here are four operation methods.

  • Set.prototype.add(value): adds a value and returns the set structure itself.
  • Set.prototype.delete(value): deletes a value and returns a Boolean value indicating whether the deletion is successful.
  • Set.prototype.has(value): returns a Boolean value indicating whether the value is a member of set.
  • Set.prototype.clear(): clear all members without return value.

Examples of these properties and methods are as follows.

s.add(1).add(2).add(2);
// Note 2 was 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 if the Object structure and the Set structure are written differently in determining whether a key is included.

// Object writing
const properties = {
  'width': 1,
  'height': 1
};

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

// Writing method of 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 an array.

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

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

Traversal operation

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

  • Set.prototype.keys(): the traverser that returns the key name
  • Set.prototype.values(): the traverser that returns the key value
  • Set.prototype.entries(): returns the traversal of key value pairs
  • Set.prototype.forEach(): use the callback function to traverse each member

It should be noted that the traversal order of Set is the insertion order. This feature is sometimes very useful. For example, using Set to save a list of callback functions can ensure that they are called in the order of addition.

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

The keys method, values method and entries method all return ergodic objects. Since the Set structure has no key name and only key value (or the key name and key value are the same value), the behaviors 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 key names and key values, so each time an array is output, its two members are exactly equal.

The instance of the Set structure is traversable by default, and its default iterator generation function is its values method.

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

This means that you can omit the values method and directly iterate over the Set with the for...of loop.

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

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

(2)forEach()

Like an array, an instance of a Set structure also has a forEach method to perform certain operations on each member without returning a 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, followed by the key value, key name and the Set itself (this parameter is omitted in the above example). It should be noted here that the key name of the Set structure is the key value (both 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 extension operator (...) uses a 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 extension operator and Set structure can remove the 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 the array 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, Union, Intersect and Difference can be easily realized by using Set.

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}

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

At present, there is no direct method to synchronously change the original Set structure in the traversal operation, but there are two alternative methods. One is to use the original Set structure to map a new structure, and then assign a value to the original Set structure; The other is to use the Array.from method.

// Method 1
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// The values of set are 2, 4, 6

// Method 2
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// The values of set are 2, 4, 6

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

WeakSet

meaning

The WeakSet structure is similar to Set, and it is also a collection of non duplicate values. However, it differs from Set in two ways.

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

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 tries to add a value and Symbol value to the WeakSet, but an error is reported because the WeakSet can only place objects.

Secondly, all objects in the WeakSet are weak references, that is, the garbage collection mechanism does not consider the reference of the WeakSet to the object, that is, if other objects no longer reference the object, the garbage collection mechanism will automatically recycle the memory occupied by the object, regardless of the object still existing in the WeakSet.

This is because the garbage collection mechanism judges the collection according to the reachability of the object. If the object can still be accessed, the garbage collection mechanism will not release this memory. After using this value, sometimes you forget to dereference, resulting in memory can not be released, which may lead to memory leakage. The references in the 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 information bound to objects. As long as these objects disappear externally, their references in the WeakSet will disappear automatically.

Due to the above characteristics, the member of WeakSet is not suitable for reference because it will disappear at any time. In addition, 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 the operation time of the garbage collection mechanism is unpredictable. Therefore, ES6 stipulates that the WeakSet cannot be traversed.

These features also apply to the WeakMap structure to be introduced later in this chapter.

grammar

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 an array like object as a parameter. (in fact, any object with Iterable interface can be used as a parameter of the WeakSet.) all members of the array will automatically become members of the instance object of the WeakSet.

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 of which are arrays. Take a as the parameter of the WeakSet constructor, and the members of a will automatically become members of the WeakSet.

Note that it is the members of the a array that become members of the WeakSet, not the a array itself. This means that the members of an 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, if the member of array b is not an object, an error will be reported if you add WeakSet.

The WeakSet structure has the following three methods.

  • WeakSet.prototype.add(value): adds a new member to the WeakSet instance.
  • WeakSet.prototype.delete(value): clears the specified members of the WeakSet instance.
  • WeakSet.prototype.has(value): returns a Boolean value indicating whether a value is in the WeakSet instance.

Here 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

The WeakSet has no size attribute, 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 attempts to obtain the size and forEach attributes, but the results are unsuccessful.

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

Here 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 Only in Foo Called on an instance of!');
    }
  }
}

The above code ensures 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 included in the memory recovery mechanism. Therefore, when deleting the instance, foos does not need to be considered and memory leakage will not occur.

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 brings great restrictions to 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 a string [object HTMLDivElement].

To solve this problem, ES6 provides a Map data structure. It is similar to an Object and a collection of key value pairs, but the range of "key" is not limited to strings. Various types of values (including objects) can be used as keys. In other words, the Object structure provides "string value" correspondence, and the Map structure provides "value value" correspondence, which is a more perfect Hash structure implementation. If you need a "key value pair" data structure, Map is more appropriate 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 a Map. As a constructor, Map can also accept an array as a parameter. The members of the array are arrays representing 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"

When creating a new Map instance, the above code specifies two keys: name and title '.

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, any data structure with Iterator interface and each member is a two element array can be used as a parameter of the Map constructor. That is to say, 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 as the parameters of the Map constructor respectively. As a result, new Map objects are generated.

If the same key is assigned multiple times, the subsequent 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 two consecutive values to key 1, and the latter value overwrites the previous value.

undefined if an unknown key is read.

new Map().get('asfddfsasadf')
// undefined

Note that only references to the same object are treated as the same key by the Map structure. Be very careful about this.

const map = new Map();

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

The set and get methods in the above code are ostensibly for the same key, but in fact they are two different array instances with different memory addresses. Therefore, 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 variables k1 and k2 are the same, but they are regarded as two keys in the Map structure.

It can be seen from the above that the key of 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 attribute collision with the same name. When we expand other people's libraries, if we use the object as the key name, we don't have to worry about our own attribute with the same name as the original author's attribute.

If the key of a Map is a simple type of value (number, string, Boolean value), as long as the two values are strictly equal, the Map regards it as a key. For example, 0 and - 0 are one key, and the Boolean value true and 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 properties and operation methods

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

(1) size attribute

The size property 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 the 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)        // The key is a string
m.set(262, 'standard')     // Keys are numeric values
m.set(undefined, 'nah')    // The key is undefined

The set method returns the current Map object, so it can be written in chain.

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 the key. If the key cannot be found, it returns undefined.

const m = new Map();

const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // Keys are functions

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 and returns no 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 ergodic generating functions and one ergodic method.

  • Map.prototype.keys(): the traverser that returns the key name.
  • Map.prototype.values(): an iterator that returns key values.
  • Map.prototype.entries(): returns the traversal of all members.
  • Map.prototype.forEach(): traverse all members of the map.

It should be noted 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"

// perhaps
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 last example of the above code represents the default iterator interface (Symbol.iterator attribute) of the Map structure, which is the entries method.

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

A quick way to convert a Map structure to an array structure is to use the extension 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']]

Combined with the Map method and filter method of array, the traversal and filtering of Map can be realized (Map itself has no 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, which is similar to the forEach method of array, and can also be traversed.

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

The forEach method can also accept the second parameter 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, this of the callback function of the forEach method points to the reporter.

Conversion with other data structures

(1) Convert Map to array

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

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

(2) Convert array to Map

If you pass the array into the Map constructor, you can turn it into a Map.

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

(3) Map to object

If all Map keys are strings, it can be converted to objects without damage.

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, the key name will be converted into a string and used as the key name of the object.

(4) Object to Map

Object can be converted to Map through 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

Two situations should be distinguished when converting Map to JSON. In one case, the key names of the Map are all strings. In this case, you can choose to convert them to object JSON.

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

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

In another case, if the key name of the Map has a non string, you can choose to convert it to array 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. The whole JSON is an array, and each array member itself is an array with two members. At this time, it can be converted to Map one by one. This is often the reverse operation of converting a Map into an array JSON.

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 WeakMap structure is similar to the Map structure and is also a collection used to generate key value pairs.

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

// WeakMap can also accept an array,
// As an argument to 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, WeakMap only accepts objects as key names (except null), and does not accept other types of values as key names.

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 value 1 and Symbol are used as the key names of the WeakMap, an error will be reported.

Secondly, the object pointed to by the key name of WeakMap is not included 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 the object. Look at 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 arr reference to e1 and e2.

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

// When e1 and e2 are not needed
// You must delete the reference manually
arr [0] = null;
arr [1] = null;

It's obviously inconvenient to write like this. Once you forget to write, it will cause memory leakage.

WeakMap was born to solve this problem. The objects referenced by its key name are weak references, that is, the garbage collection mechanism does not take this reference into account. Therefore, as long as other references of the referenced object are cleared, the garbage collection mechanism will free 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 the WeakMap will disappear automatically without manually deleting the reference.

Basically, if you want to add data to an object without interfering with the garbage collection mechanism, you can use WeakMap. A typical application scenario is that you can use the WeakMap structure by adding data to the DOM element of the web page. When the DOM element is cleared, its 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 new WeakMap instance. Then, a DOM node is stored in the instance as the key name, and some additional information is stored in the WeakMap as the key value. At this time, the reference to element in the WeakMap is a weak reference and will not be included in the garbage collection mechanism.

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

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

Note that the WeakMap weak reference is only 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 obj is a normal reference. Therefore, even if the reference of obj is eliminated outside the WeakMap, the reference inside the WeakMap still exists.

Syntax of WeakMap

There are two main API differences between WeakMap and Map. One is that there is no traversal operation (that is, there are no keys(), values() and entries() methods) and no size attribute. Because there is no way to list all key names, the existence of a key name is completely unpredictable, which is related to the operation of the garbage collection mechanism. At this moment, the key name can be obtained. At the next moment, when the garbage collection mechanism suddenly runs, the key name will disappear. In order to prevent uncertainty, it is uniformly stipulated that the key name cannot be obtained. Second, it cannot be emptied, that is, the clear method is not supported. Therefore, there are only four methods available for WeakMap: get(), set(), has(), delete().

const wm = new WeakMap();

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

Example of WeakMap

The example of WeakMap is difficult to demonstrate because the references in it will disappear automatically if you can't observe it. At this time, other references are released, and there is no reference to the key name pointing to WeakMap, so it is impossible to verify whether the key name exists.

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 memory usage status obtained is accurate
> global.gc();
undefined

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

> let wm = new WeakMap();
undefined

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

// Set the key name of the WeakMap instance and also point 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 the second time
// However, WeakMap is a weak reference, and the reference count is still 1 for the engine
> wm.set(key, 1);
WeakMap {}

> global.gc();
undefined

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

// Clear the reference of variable key to the array,
// However, the reference to the array by the key name of the WeakMap instance is not cleared manually
> key = null;
null

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

// The memory occupied heapUsed changes back to about 4M,
// You can see that the key name reference of WeakMap does not prevent gc from reclaiming 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, it will be much easier to solve memory leaks.

The Memory panel of Dev Tools in Chrome browser has a trash can button to force garbage collect ion. This button can also be used to observe whether the reference in the WeakMap disappears.

Purpose of WeakMap

As mentioned earlier, a typical case of a WeakMap application is a DOM node as a key name. Here 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 that updates the status whenever a click event occurs. We put this state as a 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 leakage.

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, there are two internal properties of the Countdown class_ counter and_ Actions are weak references to instances, so if instances are deleted, they will disappear without memory leakage.

WeakRef

WeakSet and WeakMap are data structures based on weak references. ES2021 goes further and provides a WeakRef object to directly create weak references to objects.

let target = {};
let wr = new WeakRef(target);

In the above example, target is the original object, and the constructor WeakRef() creates a new target based object wr. Here, wr is an instance of WeakRef, which is a weak reference to the target. The garbage collection mechanism will not include this reference, that is, the reference of wr will not prevent the original object target from being cleared by the garbage collection mechanism.

The WeakRef instance object has a deref() method. If the original object exists, this method returns the original object; If the original object has been cleared by the garbage collection mechanism, the method returns undefined.

let target = {};
let wr = new WeakRef(target);

let obj = wr.deref();
if (obj) { // target was not cleared by the garbage collection mechanism
  // ...
}

In the above example, the deref() method can determine whether the original object has been cleared.

A great use of weak reference objects is as a cache. When they are not cleared, they can take values from the cache. Once the cache is cleared, it will automatically become invalid.

function makeWeakCached(f) {
  const cache = new Map();
  return key => {
    const ref = cache.get(key);
    if (ref) {
      const cached = ref.deref();
      if (cached !== undefined) return cached;
    }

    const fresh = f(key);
    cache.set(key, new WeakRef(fresh));
    return fresh;
  };
}

const getImageCached = makeWeakCached(getImage);

In the above example, makeWeakCached() is used to create a cache that holds weak references to the original file.

Note that the standard stipulates that once the weak reference of the original object is created by using WeakRef(), the original object will not be cleared in this event loop, but only in the later event loop.

FinalizationRegistry

ES2021 introduces the cleaner registry function FinalizationRegistry, which is used to specify the callback function to be executed after the target object is cleared by the garbage collection mechanism.

First, create a new registry instance.

const registry = new FinalizationRegistry(heldValue => {
  // ....
});

In the above code, finalizationregistry () is the constructor provided by the system, which returns a cleaner registry instance in which the callback function to be executed is registered. The callback function is passed in as a parameter of FinalizationRegistry(), which itself has a parameter heldValue.

Then, the register() method of the registry instance is used to register the target object to be observed.

registry.register(theObject, "some value");

In the above example, theObject is the target object to be observed. Once the object is cleared by the garbage collection mechanism, the registry will invoke the callback function registered earlier when it is cleared, and use some value as the parameter (the heldValue ahead) to pass the callback function.

Note that the registry does not constitute a strong reference to the target object theObject, which is a weak reference. Because of strong references, the original object will not be cleared by the garbage collection mechanism, which loses the significance of using the registry.

The parameter heldValue of the callback function can be any type of value, string, numeric value, Boolean value, object, or even undefined.

Finally, if you want to cancel the registered callback function in the future, you need to pass a third parameter to register() as the tag value. This tag value must be an object, usually the original object. Then, unregister using the unregister() method of the registry instance object.

registry.register(theObject, "some value", theObject);
// ... other actions
registry.unregister(theObject);

In the above code, the third parameter of the register() method is the tag value theObject. When canceling the callback function, use the unregister() method with the tag value as the parameter of the method. Here, the reference of the register() method to the third parameter is also a weak reference. Without this parameter, the callback function cannot be cancelled.

Since the callback function no longer exists in the registry after it is called, the unregister() should be executed before the callback function is called.

Next, we use finalization registry to enhance the cache function in the previous section.

function makeWeakCached(f) {
  const cache = new Map();
  const cleanup = new FinalizationRegistry(key => {
    const ref = cache.get(key);
    if (ref && !ref.deref()) cache.delete(key);
  });

  return key => {
    const ref = cache.get(key);
    if (ref) {
      const cached = ref.deref();
      if (cached !== undefined) return cached;
    }

    const fresh = f(key);
    cache.set(key, new WeakRef(fresh));
    cleanup.register(fresh, key);
    return fresh;
  };
}

const getImageCached = makeWeakCached(getImage);

Compared with the example in the previous section, the above example adds a cleaner registry. Once the cached original objects are cleared by the garbage collection mechanism, a callback function will be automatically executed. The callback function will clear the expired keys in the cache.

Here is another example.

class Thingy {
  #file;
  #cleanup = file => {
    console.error(
      `The \`release\` method was never called for the \`Thingy\` for the file "${file.name}"`
    );
  };
  #registry = new FinalizationRegistry(this.#cleanup);

  constructor(filename) {
    this.#file = File.open(filename);
    this.#registry.register(this, this.#file, this.#file);
  }

  release() {
    if (this.#file) {
      this.#registry.unregister(this.#file);
      File.close(this.#file);
      this.#file = null;
    }
  }
}

In the above example, if, for some reason, the instance object of Thingy class is cleared by the garbage collection mechanism without calling the release() method, the cleaner will call the callback function #cleanup() and output an error message.

Since it is impossible to know when the cleaner will execute, it is best to avoid using it. In addition, if the browser window closes or the process exits unexpectedly, the cleaner will not run.

Tags: Javascript Front-end html5

Posted on Wed, 01 Dec 2021 14:51:28 -0500 by LostNights