JavaScript WeakMap e WeakSet
In questo capitolo forniamo informazioni complete su WeakMap e WeakSet, strumenti utili per archiviare dati in JavaScript.
Introduzione a JavaScript WeakMap e WeakSet
WeakMap e WeakSet sono versioni specializzate di Map e Set. Dal punto di vista dell'utilizzo sono quasi identici, ma si differenziano in un aspetto decisivo: conservano le loro chiavi (o membri) in modo debole. Un riferimento debole non impedisce al motore JavaScript di rimuovere un object dalla memoria. Se l'unica cosa che punta a un object è una chiave di WeakMap o un membro di WeakSet, l'object può comunque essere sottoposto a garbage collection.
Questa singola proprietà conferisce a WeakMap e WeakSet il loro scopo: permettono di associare dati aggiuntivi a un object senza controllarne il ciclo di vita. Quando l'object scompare, i dati associati scompaiono con lui — automaticamente, senza alcun codice di pulizia.
Questo capitolo tratta le regole che li distinguono, i compromessi che tali regole impongono e le situazioni reali in cui sono lo strumento giusto.
Cosa significa "debole"
In una Map normale, una chiave è un riferimento forte. Finché la Map esiste, ogni chiave che contiene viene mantenuta in vita — anche se nessun'altra parte del programma la utilizza. Questa è una fonte comune di perdite di memoria: si dimentica di usare delete su una voce e l'object chiave rimane per sempre.
Una WeakMap inverte questo comportamento. La chiave è referenziata debolmente, quindi il motore è libero di recuperare l'object chiave non appena nient'altro vi fa riferimento. Quando ciò avviene, la voce scompare semplicemente dalla WeakMap.
let visits = new WeakMap();
let user = { name: "Alice" };
visits.set(user, 10);
console.log(visits.get(user)); // 10
console.log(visits.has(user)); // true
user = null; // the object is now unreachable elsewhere
// The WeakMap entry becomes eligible for garbage collection.
// You cannot observe exactly when it is removed.WeakMap
Una WeakMap è una raccolta di coppie chiave/valore in cui le chiavi devono essere object e i valori possono essere di qualsiasi tipo.
Le chiavi devono essere object
I valori primitivi (string, numbers, boolean, symbol, null, undefined) non possono essere chiavi. La ragione deriva dalla progettazione: i primitivi non vengono sottoposti a garbage collection come gli object, quindi "tenerli debolmente" non ha senso. Il tentativo di usarne uno genera un TypeError.
let wm = new WeakMap();
wm.set("a string", 1); // TypeError: Invalid value used as weak map keyMetodi disponibili
Una WeakMap supporta solo quattro operazioni:
set(key, value)— memorizza un valore associato a una chiave object.get(key)— legge il valore, oundefinedse assente.has(key)— verifica se una chiave esiste.delete(key)— rimuove una voce.
let wm = new WeakMap();
let key = { id: 1 };
wm.set(key, "data");
console.log(wm.has(key)); // true
console.log(wm.get(key)); // "data"
wm.delete(key);
console.log(wm.has(key)); // falseNessuna iterazione e nessuna dimensione
Non esiste né la proprietà size, né clear(), né alcun modo per iterare una WeakMap (nessun keys(), values(), entries() o forEach). Non si tratta di una svista. Poiché le voci possono scomparire in qualsiasi momento quando il garbage collector viene eseguito, i contenuti sono non deterministici — esporli permetterebbe al codice di osservare i tempi del GC, cosa che il linguaggio nasconde deliberatamente.
let wm = new WeakMap();
console.log("size" in wm); // false
console.log(wm[Symbol.iterator]); // undefinedSe è necessario contare le voci, iterare o archiviare chiavi primitive, utilizzare una Map normale.
WeakSet
Un WeakSet è l'equivalente di Set: una raccolta di object unici conservati debolmente. Come WeakMap, i suoi membri devono essere object e non offre iterazione né size.
let visited = new WeakSet();
let a = { id: 1 };
let b = { id: 2 };
visited.add(a);
console.log(visited.has(a)); // true
console.log(visited.has(b)); // false
visited.delete(a);
console.log(visited.has(a)); // falseLa sua API completa è semplicemente add(value), has(value) e delete(value).
Casi d'uso pratici
Caching e memoization
Memorizza nella cache il risultato di un'operazione costosa utilizzando come chiave l'object a cui si riferisce. Poiché la chiave è debole, la cache non mantiene mai in vita un object non più necessario.
const cache = new WeakMap();
function process(obj) {
if (cache.has(obj)) {
return cache.get(obj); // reuse the cached result
}
const result = obj.value * 2; // pretend this is expensive
cache.set(obj, result);
return result;
}
let data = { value: 21 };
console.log(process(data)); // 42 (computed)
console.log(process(data)); // 42 (from cache)Dati privati per object
Archivia dati che appartengono a un object senza aggiungere una proprietà all'object stesso (che chiunque potrebbe leggere o sovrascrivere). Questo è un modo classico per emulare campi veramente privati, correlato a come i metodi si legano agli object tramite this.
const balances = new WeakMap();
class Account {
constructor(amount) {
balances.set(this, amount); // private to this module
}
deposit(n) {
balances.set(this, balances.get(this) + n);
}
get balance() {
return balances.get(this);
}
}
const acc = new Account(100);
acc.deposit(50);
console.log(acc.balance); // 150
// There is no `amount` property on `acc` itself to tamper with.Quando l'istanza di Account non è più referenziata, la sua voce del saldo viene raccolta automaticamente.
Metadati per i nodi DOM
Una perdita frequente nelle pagine a lunga esecuzione consiste nel mantenere una Map di metadati dei nodi DOM dopo che i nodi vengono rimossi dal documento. Con un WeakSet/WeakMap, una volta che un nodo viene scollegato e dereferenziato, i suoi metadati vengono recuperati anch'essi.
const seen = new WeakSet();
function markVisited(node) {
if (seen.has(node)) return false; // already processed
seen.add(node);
return true;
}
// When the node leaves the DOM and nothing else holds it,
// it disappears from `seen` without manual cleanup.WeakMap/WeakSet vs Map/Set
| Capacità | Map / Set | WeakMap / WeakSet |
|---|---|---|
| Chiavi / membri | Qualsiasi valore | Solo object |
| Forza del riferimento | Forte (mantiene le chiavi in vita) | Debole (consente il GC) |
size, clear() | Sì | No |
Iterazione (forEach, keys…) | Sì | No |
| Ideale per | Raccolte generali | Dati associati a object con pulizia automatica |
Scegli la variante debole solo quando vuoi specificamente che il motore gestisca la pulizia per te e non hai bisogno di enumerare i contenuti. Per tutto il resto, utilizza la normale Map e Set.