Quickly learn about ES6's Map and WeakMap

Before ECMAScript 6, you can use the Object attribute of the Object as the key, and then use the attribute reference value to store the key/value; In ECMAScript 6 specification, Map and WeakMap are added to store key/value.

Map

The new Map collection in ECMAScript 6 can create an empty Map using new and Map constructors:

const map = new Map();

You can also insert key value pairs into a new mapping instance in iterative order when creating a mapping:

// Initialize mapping using nested arrays
const m1 = new Map([
    ["name", "Xiao Zhao"],
    ["age", 12],
    ["sex", "male"]
]);
alert(m1.size); // 3

// Initializing a map using a custom iterator
const m2 = new Map({
    [Symbol.iterator]: function*() {
        yield ["name", "Xiao Zhao"];
        yield ["age", 12];
        yield ["sex", "male"];
    }
});
alert(m2.size); // 3

// Map the expected key / value pair, whether or not provided
const m3 = new Map([[]]);
alert(m3.has(undefined)); // true
alert(m3.get(undefined)); // undefined

API

After initialization, if you want to add key/value to the mapping table, you need to use the set() method:

const map = new Map();
map.set("name", "Xiao Zhao");
map.set("age", 24);
map.set("sex", "male");

However, the set() method returns a Map instance, so it can be initialized. You can declare as follows:

const map = new Map().set("name", "Xiao Zhao");
map.set("age", 24).set("sex", "male");

The stored key/value can be queried through the get() and has() methods. The has() method is used to determine whether the key exists in the Map, and get() is used to obtain the value corresponding to the key:

console.log(map.has("score"));    // false
console.log(map.get("name"));    // "Xiao Zhao"

You can obtain the number of keys / values in the Map through the size attribute:

console.log(map.size);    // 3

If you want to delete the key/value in the Map, you can use the delete() and clear() methods. The delete() method is used to delete the specified key, and the clear() method is used to clear all the key / values in the Map:

map.delete("age");
console.log(map.size);    // 2
map.clear();
console.log(map.size);    // 0

If you need to return the [key, value] array of each element in the Map in insertion order, you can use the entries() method or the Symbol.iterator attribute to obtain the iterator in the Map.

const m = new Map([
    ["name", "Xiao Zhao"],
    ["age", 12],
    ["sex", "male"]
]);
console.log(m.entries === m[Symbol.iterator]); // true
for (let kv of m.entries()) {
    console.log(kv);
}
// ['name', 'Xiao Zhao']
// [ 'age', 12 ]
// ['sex', 'male']
for (let kv of m[Symbol.iterator]()) {
    console.log(kv);
}
// ['name', 'Xiao Zhao']
// [ 'age', 12 ]
// ['sex', 'male']
for (let [k, v] of m.entries()) {
    console.log(k + " = " + v);
}
// name = Xiao Zhao
// age = 12
// sex = male

Since entries() is the default iterator, you can directly use the extension operation on the Map instance to convert the Map into an array:

const m = new Map([
    ["name", "Xiao Zhao"],
    ["age", 12],
    ["sex", "male"]
]);
console.log([...m]); // [['name ',' Xiao Zhao '], ['age', 12], ['sex ',' male ']]

You can also use the forEach(callback, opt_thisArg) method and pass in a callback to iterate over each key/value in turn. The incoming callback receives an optional second parameter, which is used to override the value of this inside the callback:

m.forEach((val, key) => console.log(`${key} -> ${val}`));
// Name - > Xiao Zhao
// age -> 12
// Sex - > male

keys() and values() return iterators that generate key s and values in insertion order, respectively:

for (let k of m.keys()) {
    console.log(k);
}
// name
// age
// sex
for (let v of m.values()) {
    console.log(v);
}
// Xiao Zhao
// 12
// male

Keys and values can be modified during iterator traversal, but references inside the map cannot be modified. Of course, this does not prevent you from modifying properties inside objects that are keys or values, because this does not affect their identity in the mapping instance:

const m1 = new Map([
    ["name", "Xiao Zhao"]
]);
// The original value of the string as a key cannot be modified
for (let key of m1.keys()) {
    key = 10010;
    console.log(key); // 10010
    console.log(m1.get("name")); // Xiao Zhao
}
const key2 = {id: 1};
const m2 = new Map([
    [key2, "Xiaoxin"]
]);
// The properties of the object as the key are modified, but the object still references the same value within the mapping
for (let key of m2.keys()) {
    key.id = 10086;
    console.log(key); // {id: 10086}
    console.log(m2.get(key2)); // Xiaoxin
}
console.log(key2); // {id: 10086}

Bond equality

In Map, key comparison is based on someValueZero algorithm:

  • NaN is equal to NaN (although NaN! = = NaN), all other values are determined according to the result of the = = = operator.
  • In the current ECMAScript specification, - 0 and + 0 are considered equal, although this was not the case in earlier drafts.

Compare with Object

Like Object, Map allows you to access value by key, delete key, and check whether the key is bound to value. However, Map and Object do have significant differences in memory and performance.

Generally, the memory occupied by storing a single key/value will increase linearly with the number of keys, and will also be affected by different browsers. However, for the specified size of memory, Map can store more key/value than Object; If a large number of inserts are involved, the Map specially designed for storing key/value has better performance.

For a Map, the key stored in the Map can be any value, and the order of keys is determined by the order of insertion; The delete() method is also faster than insertion and lookup.

The key of an Object can only be String or Symbol, and the insertion order is out of order; In addition, when you delete a property in an Object, some pseudo deletions will occur, including setting the property value to undefined or null.

WeakMap

WeakMap is also a new mapping table in ECMAScript 6, and it is also a subset of Map. However, the key stored in the WeakMap must be of object type and "weak mapping", which will cause the value referred to by the key to be GC recycled when it is not referenced elsewhere; Value can be of any type.

The API s of WeakMap and Map are basically the same, but the keys in WeakMap can only be objects or types inherited from objects. Trying to use non Object setting keys will throw typeerrors, and there are no restrictions on the types of values.

However, WeakMap has abandoned the clear() method, but it can be achieved by creating an empty WeakMap and replacing the original object.

let wm = new WeakMap();
vm.set("name", "Small money").set("age", 24).set("sex", "male");

// Empty WeakMap
vm = new WeakMap();

Weak bond

"weak" in the WeakMap means that the key exists, and the key/value will exist in the WeakMap as a reference to value and will not be recycled by GC; When the key does not exist, GC recycling will not be prevented.

const wm = new WeakMap();
wm.set({}, "weak value");

The set() method initializes a new object and uses it as a key for a string. Because there are no other references to this object, the object key will be garbage collected when this line of code is executed. Then, the key / value pair disappears from the weak mapping, making it an empty mapping. In this example, because the value is not referenced, the value itself will become the target of garbage collection after the pair of keys / values are destroyed. Take a slightly different example:

const wm = new WeakMap();
const container = {
    key: {}
};
wm.set(container.key, "weak val");
function removeReference() {
    container.key = null;
}

This time, the container object maintains a reference to container.key, so the object key will not be recycled by GC. However, if removeReference() is called, the last reference of the key will be destroyed and recycled by the GC.

Non iteratable key

Because the key/value in the WeakMap can be destroyed at any time, there is no iterative method such as entries(), keys and values, and there is no clear() method to clear all the key / values at one time.

The reason why the object can only be used as a key is to ensure that the value can be obtained only through the reference of the key. If the original value is allowed, there is no way to distinguish between the literal amount of the string used during initialization and an equal string used after initialization.

application

Due to the weak reference of key in WeakMap, it can be applied in many aspects.

private variable

Private variables are not accessible to the outside world and cannot be shared by multiple instances, so they can be implemented using WeakMap. As follows:

const wm = new WeakMap();
class User {
    constructor(id) {
        this.idProperty = Symbol('id');
        this.setId(id);
    }
    setPrivate(property, value) {
        const privateMembers = wm.get(this) || {};
        privateMembers[property] = value;
        wm.set(this, privateMembers);
    }
    getPrivate(property) {
        return wm.get(this)[property];
   }
    setId(id) {
        this.setPrivate(this.idProperty, id);
    }
    getId() {
        return this.getPrivate(this.idProperty);
    }
}
const user = new User(123);
alert(user.getId()); // 123
user.setId(456);
alert(user.getId()); // 456
// It's not really private
alert(wm.get(user)[user.idProperty]); // 456

For the above implementation, the external only needs to get the reference and weak mapping of the object instance to get the "private" variable. In order to avoid such access, you can wrap the WeakMap with a closure, so that the weak map can be completely isolated from the outside world:

const User = (() => {
    const wm = new WeakMap();
    class User {
        constructor(id) {
            this.idProperty = Symbol('id');
            this.setId(id);
        }
        setPrivate(property, value) {
            const privateMembers = wm.get(this) || {};
            privateMembers[property] = value;
            wm.set(this, privateMembers);
        }
        getPrivate(property) {
            return wm.get(this)[property];
        }
        setId(id) {
            this.setPrivate(this.idProperty, id);
        }
        getId(id) {
            return this.getPrivate(this.idProperty);
        }
    }
    return User;
})();
const user = new User(123);
alert(user.getId()); // 123
user.setId(456);
alert(user.getId()); // 456

In this way, the key in the weak mapping cannot be obtained, and the corresponding value in the weak mapping cannot be obtained. Although this prevents the access mentioned earlier, the whole code is completely trapped in the closed private variable mode before ES6.

DOM node metadata

When the DOM elements stored by the WeakMap instance are cleared, the corresponding WeakMap records will be recycled by GC, so it is very suitable for saving associated metadata. Let's take a look at the following example, in which a conventional Map is used:

const m = new Map();
const lb = document.querySelector('#login');
// Associate some metadata with this node
m.set(lb, {disabled: true});

Suppose that after the above code is executed, the page is changed by JavaScript, and the original login button is deleted from the DOM tree. However, because the reference of the button is also saved in the map, the corresponding DOM node will still stay in memory unless it is explicitly deleted from the map or until the map itself is destroyed.

If a weak mapping is used here, as shown in the following code, the garbage collector can release its memory immediately after the node is deleted from the DOM tree (assuming that no other place references this object):

const wm = new WeakMap();
const lb = document.querySelector('#login');
// Associate some metadata with this node
wm.set(lb, {disabled: true});

More content, please pay attention to the official account of the sea.

Keywords: Javascript Front-end ECMAScript Programmer

Added by fwegan on Tue, 23 Nov 2021 04:54:29 +0200