Set 和 Map 数据结构
# Set and Map Data Structures
# Set
# Basic Usage
ES6 provides a new data structure called Set. It is similar to an array, but the values of its members are all unique, with no duplicates.
Set is itself a constructor used to generate Set data structures.
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
2
3
4
5
6
7
8
The code above adds members to the Set structure using the add() method, demonstrating that Set does not add duplicate values.
The Set function can accept an array (or any other data structure with an 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
// Similar to
const set = new Set();
document
.querySelectorAll('div')
.forEach(div => set.add(div));
set.size // 56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In the code above, Examples 1 and 2 show the Set function accepting an array as a parameter, and Example 3 accepts an array-like object as a parameter.
The code above also demonstrates a method for removing duplicate members from an array.
// Remove duplicate members from an array
[...new Set(array)]
2
The method above can also be used to remove duplicate characters from a string.
[...new Set('ababbc')].join('')
// "abc"
2
When adding values to a Set, no type conversion occurs, so 5 and "5" are two different values. The algorithm Set uses internally to determine whether two values are different is called "Same-value-zero equality," which is similar to the strict equality operator (===). The main difference is that when adding values to a Set, NaN is considered equal to itself, whereas the strict 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}
2
3
4
5
6
The code above adds NaN to the Set instance twice, but only one is added. This shows that within a Set, two NaNs are considered equal.
Additionally, two objects are always considered unequal.
let set = new Set();
set.add({});
set.size // 1
set.add({});
set.size // 2
2
3
4
5
6
7
The code above shows that since two empty objects are not equal, they are treated as two different values.
# Properties and Methods of Set Instances
Set structure instances have the following properties:
Set.prototype.constructor: The constructor, which defaults to theSetfunction.Set.prototype.size: Returns the total number of members in theSetinstance.
Set instance methods are divided into two categories: operation methods (for manipulating data) and traversal methods (for iterating over members). Here are the four operation methods:
Set.prototype.add(value): Adds a value, returns the Set structure itself.Set.prototype.delete(value): Deletes a value, returns a boolean indicating whether the deletion was successful.Set.prototype.has(value): Returns a boolean indicating whether the value is a member of theSet.Set.prototype.clear(): Clears all members, no return value.
Here are examples of these properties and methods:
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
2
3
4
5
6
7
8
9
10
11
Below is a comparison showing the different ways Object structures and Set structures check whether a key is included.
// Object approach
const properties = {
'width': 1,
'height': 1
};
if (properties[someName]) {
// do something
}
// Set approach
const properties = new Set();
properties.add('width');
properties.add('height');
if (properties.has(someName)) {
// do something
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
The Array.from method can convert a Set structure to an array.
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);
2
This provides another way to remove duplicate members from an array.
function dedupe(array) {
return Array.from(new Set(array));
}
dedupe([1, 1, 2, 3]) // [1, 2, 3]
2
3
4
5
# Traversal Operations
Set structure instances have four traversal methods that can be used to iterate over members:
Set.prototype.keys(): Returns an iterator for key namesSet.prototype.values(): Returns an iterator for key valuesSet.prototype.entries(): Returns an iterator for key-value pairsSet.prototype.forEach(): Uses a callback function to iterate over each member
It's particularly worth noting that the traversal order of a Set is the insertion order. This feature can be very useful -- for example, using a Set to store a callback function list ensures they are called in the order they were added.
(1) keys(), values(), entries()
The keys, values, and entries methods all return iterator objects (see the "Iterator" chapter). Since a Set structure has no key names, only key values (or rather, key names and key values are the same), the keys and values methods behave identically.
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"]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
In the code above, the entries method returns an iterator that includes both key names and key values, so each output is an array with two identical members.
A Set structure instance is iterable by default, and its default iterator generator function is its values method.
Set.prototype[Symbol.iterator] === Set.prototype.values
// true
2
This means you can omit the values method and directly use for...of to iterate over a Set.
let set = new Set(['red', 'green', 'blue']);
for (let x of set) {
console.log(x);
}
// red
// green
// blue
2
3
4
5
6
7
8
(2) forEach()
Like arrays, Set structure instances also have a forEach method, which executes an operation on each member with no return value.
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9
2
3
4
5
The code above shows that the forEach method's parameter is a handler function. The arguments of this function are consistent with those of an array's forEach -- key value, key name, and the collection itself (omitted in the example above). Note that since a Set structure's key names are the same as key values (they are the same value), the first and second arguments are always identical.
Additionally, forEach can accept a second argument to bind this within the handler function.
(3) Traversal Applications
The spread operator (...) internally uses the for...of loop, so it can also be used with Set structures.
let set = new Set(['red', 'green', 'blue']);
let arr = [...set];
// ['red', 'green', 'blue']
2
3
Combining the spread operator with a Set structure allows you to remove duplicate members from an array.
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]
2
3
Furthermore, array map and filter methods can also be used indirectly with Sets.
let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// Returns Set structure: {2, 4, 6}
let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// Returns Set structure: {2, 4}
2
3
4
5
6
7
Therefore, Sets make it easy to implement Union, Intersect, and Difference operations.
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}
// Intersect
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}
// Difference
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
2
3
4
5
6
7
8
9
10
11
12
13
14
If you want to synchronously modify the original Set structure during traversal, there is currently no direct method, but there are two workarounds. One is to map the original Set structure to a new structure and then assign it back to the original Set; 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));
// set values are 2, 4, 6
// Method 2
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set values are 2, 4, 6
2
3
4
5
6
7
8
9
The code above provides two methods for directly modifying the original Set structure during traversal.
# WeakSet
# Meaning
The WeakSet structure is similar to Set in that it is also a collection of unique values. However, it has two differences from Set.
First, WeakSet members 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
2
3
4
5
The code above tries to add a number and a Symbol value to a WeakSet, resulting in errors because WeakSet can only store objects.
Second, objects in a WeakSet are weak references, meaning the garbage collector does not consider the WeakSet's reference to the object. In other words, if no other objects reference the object, the garbage collector will automatically reclaim the memory used by that object, regardless of whether the object still exists in the WeakSet.
This is because the garbage collector relies on reference counting. If the reference count for a value is not 0, the garbage collector will not free that memory. After you're done using a value, sometimes you might forget to remove the reference, causing the memory to not be freed, which may lead to memory leaks. References inside a WeakSet do not count toward the garbage collector, so this problem doesn't exist. Therefore, WeakSet is suitable for temporarily storing a group of objects, as well as storing information tied to objects. As long as these objects disappear externally, their references in the WeakSet will automatically disappear.
Due to this characteristic, WeakSet members should not be relied upon, as they may disappear at any time. Additionally, since the number of members inside a WeakSet depends on whether the garbage collector has run (and the count might differ before and after), and since garbage collection timing is unpredictable, ES6 specifies that WeakSet is not iterable.
These characteristics also apply to the WeakMap structure discussed later in this chapter.
# Syntax
WeakSet is a constructor that can be used with 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. (In fact, any object with an Iterable interface can serve as a WeakSet parameter.) All members of the array will automatically become members of the WeakSet instance.
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}
2
3
In the code above, a is an array with two members, both of which are arrays. Passing a as the parameter to the WeakSet constructor, the members of a automatically become members of the WeakSet.
Note that it is the members of array a that become WeakSet members, not array a itself. This means the array members must be objects.
const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
2
3
In the code above, the members of array b are not objects, so adding them to a WeakSet throws an error.
The WeakSet structure has the following three methods:
- WeakSet.prototype.add(value): Adds a new member to the WeakSet instance.
- WeakSet.prototype.delete(value): Removes a specified member from the WeakSet instance.
- WeakSet.prototype.has(value): Returns a boolean indicating whether a value exists 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
2
3
4
5
6
7
8
9
10
11
12
WeakSet does not have a size property and has no way to iterate over its members.
ws.size // undefined
ws.forEach // undefined
ws.forEach(function(item){ console.log('WeakSet has ' + item)})
// TypeError: undefined is not a function
2
3
4
5
The code above tries to access the size and forEach properties, but neither succeeds.
WeakSet cannot be iterated because its members are all weak references and may disappear at any time. The iteration mechanism cannot guarantee the existence of members -- they might be gone by the time iteration ends. One use of WeakSet is to store DOM nodes without worrying about memory leaks when those 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 can only be called on Foo instances!');
}
}
}
2
3
4
5
6
7
8
9
10
11
The code above ensures that Foo's instance methods can only be called on Foo instances. The advantage of using WeakSet here is that foos's references to instances are not counted by the memory garbage collection mechanism, so you don't need to worry about foos when deleting instances, and there will be no memory leaks.
# Map
# Meaning and Basic Usage
JavaScript objects (Object) are essentially collections of key-value pairs (Hash structures), but traditionally they can only use strings as keys. This is a significant limitation.
const data = {};
const element = document.getElementById('myDiv');
data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"
2
3
4
5
The original intent of the code above is to use a DOM node as a key of the data object. However, since objects only accept strings as key names, element is automatically converted to the string [object HTMLDivElement].
To solve this problem, ES6 provides the Map data structure. It is similar to objects and is also a collection of key-value pairs, but the range of "keys" is not limited to strings -- all types of values (including objects) can be used as keys. In other words, while the Object structure provides "string-to-value" mapping, Map provides "value-to-value" mapping, which is a more complete Hash structure implementation. 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
2
3
4
5
6
7
8
9
The code above uses the Map structure's set method to use object o as a key of m, then uses get to read this key, and then uses delete to remove this key.
The example above shows how to add members to a Map. As a constructor, Map can also accept an array as a parameter. The array's members are individual arrays representing key-value pairs.
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
2
3
4
5
6
7
8
9
10
In the code above, when creating the Map instance, two keys name and title are specified.
When the Map constructor accepts an array as a parameter, it actually executes the following algorithm.
const items = [
['name', '张三'],
['title', 'Author']
];
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value)
);
2
3
4
5
6
7
8
9
10
In fact, not just arrays, but any data structure with an Iterator interface where each member is a two-element array (see the "Iterator" chapter) can be used as a Map constructor parameter. This means both Set and Map can be used to create new Maps.
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
2
3
4
5
6
7
8
9
10
In the code above, we use both a Set object and a Map object as parameters for the Map constructor, both resulting in new Map objects.
If the same key is assigned a value multiple times, the later value will override the earlier one.
const map = new Map();
map
.set(1, 'aaa')
.set(1, 'bbb');
map.get(1) // "bbb"
2
3
4
5
6
7
The code above assigns a value to key 1 twice consecutively, with the later value overriding the earlier one.
Reading an unknown key returns undefined.
new Map().get('asfddfsasadf')
// undefined
2
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
2
3
4
The set and get methods in the code above appear to target the same key, but they are actually two different array instances with different memory addresses, so get cannot read the key and returns undefined.
Similarly, two instances with the same value are treated as two different keys in a 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
2
3
4
5
6
7
8
9
10
11
In the code above, the values of variables k1 and k2 are the same, but they are treated as two different keys in the Map structure.
From the above, we can see that Map keys are actually bound to memory addresses -- as long as the memory addresses are different, they are considered two different keys. This solves the problem of same-named property collisions (clash). When extending other people's libraries, if you use objects as key names, you don't need to worry about your properties having the same names as the original author's properties.
If a Map key is a simple type value (number, string, boolean), as long as two values are strictly equal, the Map treats them as the same key. For example, 0 and -0 are the same key, and the boolean true and the string true are two different keys. Also, undefined and null are 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Instance Properties and Methods
Map structure instances have the following properties and methods.
(1) The size Property
The size property returns the total number of members in the Map structure.
const map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
2
3
4
5
(2) Map.prototype.set(key, value)
The set method sets the key key to the value value, then returns the entire Map structure. If key already has a value, the value is updated; otherwise, a new key is generated.
const m = new Map();
m.set('edition', 6) // key is a string
m.set(262, 'standard') // key is a number
m.set(undefined, 'nah') // key is undefined
2
3
4
5
The set method returns the current Map object, so it supports chaining.
let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
2
3
4
(3) Map.prototype.get(key)
The get method reads the value corresponding to key. If key is not found, it returns undefined.
const m = new Map();
const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // key is a function
m.get(hello) // Hello ES6!
2
3
4
5
6
(4) Map.prototype.has(key)
The has method returns a boolean indicating whether a key exists 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
2
3
4
5
6
7
8
9
10
(5) Map.prototype.delete(key)
The delete method deletes a key and returns true. If the deletion fails, it returns false.
const m = new Map();
m.set(undefined, 'nah');
m.has(undefined) // true
m.delete(undefined)
m.has(undefined) // false
2
3
4
5
6
(6) Map.prototype.clear()
The clear method removes all members with no return value.
let map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
map.clear()
map.size // 0
2
3
4
5
6
7
# Traversal Methods
The Map structure natively provides three iterator generator functions and one traversal method.
Map.prototype.keys(): Returns an iterator for key names.Map.prototype.values(): Returns an iterator for key values.Map.prototype.entries(): Returns an iterator for all members.Map.prototype.forEach(): Traverses all members of the Map.
It's particularly important to note that Map's traversal order 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"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
The last example in the code above shows that the default iterator interface (Symbol.iterator property) of the Map structure is the entries method.
map[Symbol.iterator] === map.entries
// true
2
A quick way to convert a 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']]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
By combining array map and filter methods, you can implement Map traversal and filtering (Map itself does not have 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)
);
// Produces Map structure {1 => 'a', 2 => 'b'}
const map2 = new Map(
[...map0].map(([k, v]) => [k * 2, '_' + v])
);
// Produces Map structure {2 => '_a', 4 => '_b', 6 => '_c'}
2
3
4
5
6
7
8
9
10
11
12
13
14
Additionally, Map has a forEach method similar to arrays that can also be used for traversal.
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});
2
3
forEach can also accept a second argument for binding 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);
2
3
4
5
6
7
8
9
In the code above, the this in the forEach callback function refers to reporter.
# Conversions 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
3
4
5
(2) Array to Map
Passing an array to the Map constructor converts it to a Map.
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
2
3
4
5
6
7
8
(3) Map to Object
If all Map keys are strings, it can be losslessly converted to an object.
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 }
2
3
4
5
6
7
8
9
10
11
12
13
If there are non-string key names, the key name will be converted to a string and then used as the object's key name.
(4) Object to Map
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}
2
3
4
5
6
7
8
9
10
(5) Map to JSON
Converting a Map to JSON requires distinguishing between two cases. The first case is when all Map key names are strings, in which case you can convert 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}'
2
3
4
5
6
7
The second case is when Map key names include non-strings, in which case you can convert 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"]]]'
2
3
4
5
6
7
(6) JSON to Map
Normally when converting JSON to Map, all key names are strings.
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
2
3
4
5
6
However, there is a special case where the entire JSON is an array, and each array member is itself a two-element array. In this case, it can be mapped one-to-one to a Map. This is often the reverse operation of converting a Map to array JSON.
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
2
3
4
5
6
# WeakMap
# Meaning
The WeakMap structure is similar to the Map structure and is also used to generate collections 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 constructor parameter
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"
2
3
4
5
6
7
8
9
10
11
12
WeakMap differs from Map in two ways.
First, WeakMap only accepts objects as keys (null excluded) 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
2
3
4
5
6
7
In the code above, using a number 1 and a Symbol value as WeakMap keys both result in errors.
Second, the objects that WeakMap keys point to are not counted by the garbage collection mechanism.
The design purpose of WeakMap is that sometimes we want to store some data on an object, but this creates a reference to that object. Consider the following example.
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
[e1, 'foo element'],
[e2, 'bar element'],
];
2
3
4
5
6
In the code above, e1 and e2 are two objects, and we add some descriptive text to them via the arr array. This creates references from arr to e1 and e2.
Once these two objects are no longer needed, we must manually delete these references; otherwise, the garbage collector will not free the memory occupied by e1 and e2.
// When e1 and e2 are no longer needed
// References must be manually deleted
arr [0] = null;
arr [1] = null;
2
3
4
Writing code like the above is obviously inconvenient. If you forget to do it, it will cause memory leaks.
WeakMap was created to solve this problem. Its key references are all weak references, meaning the garbage collector does not consider these references. Therefore, once all other references to the referenced object are cleared, the garbage collector will free the memory occupied by that object. In other words, once no longer needed, the key-value pairs in WeakMap will automatically disappear without manually deleting references.
Essentially, if you want to add data to an object without interfering with the garbage collection mechanism, you can use WeakMap. A typical use case is adding data to DOM elements on a web page using a WeakMap structure. When the DOM element is removed, the corresponding WeakMap record is automatically removed.
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
2
3
4
5
6
In the code above, a WeakMap instance is created first. Then, a DOM node is stored as a key with some additional information as the value in the WeakMap. At this point, the WeakMap's reference to element is a weak reference and is not counted by the garbage collection mechanism.
In other words, the reference count for the DOM node object above is 1, not 2. Once the reference to that node is removed, the memory it occupies will be freed by the garbage collector. The key-value pair stored in the WeakMap will also automatically disappear.
In summary, WeakMap's specific use case is when the objects corresponding to its keys may disappear in the future. The WeakMap structure helps prevent memory leaks.
Note that WeakMap's weak reference applies only to the keys, not the values. Values are still normal references.
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};
wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}
2
3
4
5
6
7
8
In the code above, the value obj is a normal reference. So even if the reference to obj is removed outside the WeakMap, the reference inside the WeakMap still exists.
# WeakMap Syntax
WeakMap differs from Map in its API in two main ways. First, it has no traversal operations (i.e., no keys(), values(), or entries() methods), and no size property. This is because there's no way to list all key names -- whether a key exists is completely unpredictable, as it's related to whether the garbage collector has run. A key might exist at one moment and then be gone the next when the garbage collector suddenly runs. To prevent such uncertainty, the specification uniformly prohibits retrieving key names. Second, it does not support clearing, meaning the clear method is not available. Therefore, WeakMap has only four usable methods: get(), set(), has(), and delete().
const wm = new WeakMap();
// size, forEach, clear methods do not exist
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined
2
3
4
5
6
# WeakMap Examples
WeakMap examples are hard to demonstrate because there's no way to observe that its internal references automatically disappear. At that point, all other references have been removed, and there's no reference pointing to the WeakMap's key anymore, making it impossible to verify whether that key still exists.
Teacher He Shijun pointed out (opens new window) that if the referenced value occupies a particularly large amount of memory, it can be observed through Node's process.memoryUsage method. Based on this idea, user vtxf (opens new window) provided the following example.
First, open the Node command line.
$ node --expose-gc
In the code above, the --expose-gc flag enables manual garbage collection.
Then, execute the following code.
// Manually run garbage collection once to ensure accurate memory usage readings
> global.gc();
undefined
// Check the initial memory usage state, heapUsed is about 4M
> process.memoryUsage();
{ rss: 21106688,
heapTotal: 7376896,
heapUsed: 4153936,
external: 9059 }
> let wm = new WeakMap();
undefined
// Create a variable key pointing to a 5*1024*1024 array
> let key = new Array(5 * 1024 * 1024);
undefined
// Set the WeakMap instance's key to also point to the key array
// At this point, the key array is actually referenced twice:
// once by the variable key, and once by the WeakMap's key
// However, since WeakMap is a weak reference, for the engine the reference count is still 1
> wm.set(key, 1);
WeakMap {}
> global.gc();
undefined
// Now heapUsed has increased to about 45M
> process.memoryUsage();
{ rss: 67538944,
heapTotal: 7376896,
heapUsed: 45782816,
external: 8945 }
// Remove the variable key's reference to the array,
// but do not manually remove the WeakMap's key reference to the array
> key = null;
null
// Run garbage collection again
> global.gc();
undefined
// heapUsed has returned to about 4M,
// showing that the WeakMap's key reference did not prevent gc from reclaiming memory
> process.memoryUsage();
{ rss: 20639744,
heapTotal: 8425472,
heapUsed: 3979792,
external: 8956 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
In the code above, as long as external references are removed, internal references in the WeakMap are automatically cleaned up by the garbage collector. This shows that with WeakMap's help, solving memory leaks becomes much simpler.
# WeakMap Use Cases
As mentioned earlier, a typical use case for WeakMap is using DOM nodes as keys. Here is an example.
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);
2
3
4
5
6
7
8
9
In the code above, myElement is a DOM node. Every time a click event occurs, the state is updated. We store this state as a value in the WeakMap, with the corresponding key being myElement. Once this DOM node is deleted, the state will automatically disappear, with no risk of memory leaks.
Another use of WeakMap is implementing 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
In the code above, the two internal properties _counter and _action of the Countdown class are weak references to instances. So if an instance is deleted, they will disappear along with it, causing no memory leaks.