Nel ciclo di vita di un'applicazione JavaScript, soprattutto quando cresce in complessità, arriva sempre un momento in cui la gestione della memoria smette di essere invisibile. All'inizio tutto funziona, poi iniziano a comparire rallentamenti, comportamenti strani, consumi anomali. In molti casi la causa non è immediatamente evidente, perché JavaScript gestisce automaticamente la memoria attraverso il garbage collector. Non esiste un'istruzione esplicita per liberare memoria, e questo porta spesso a una falsa sensazione di sicurezza.
WeakMap, WeakSet e garbage collector in JavaScript
In realtà, il fatto che il linguaggio sia garbage collected non significa che sia immune ai memory leak. Un oggetto continua a esistere finché esiste almeno un riferimento attivo a esso. Se quel riferimento rimane, anche solo indirettamente, il garbage collector non potrà mai liberare quella memoria.
È proprio in questo contesto che strumenti come WeakMap e WeakSet diventano fondamentali. Non sono semplicemente varianti di Map e Set, ma strutture progettate per lavorare in armonia con il garbage collector, evitando che riferimenti non necessari mantengano in vita oggetti ormai inutili.
Il problema dei riferimenti forti
Per capire davvero il valore di WeakMap e WeakSet, bisogna partire dal concetto del riferimento forte. Quando si inserisce un oggetto in una Map, quella mappa mantiene un riferimento forte a quell'oggetto. Anche se il resto dell'applicazione smette di usarlo, la presenza nella Map è sufficiente a impedirne la raccolta.
Questo comportamento è corretto nella maggior parte dei casi, ma diventa problematico quando si usano strutture di caching, metadati o associazioni temporanee legate al ciclo di vita di altri oggetti. Immaginiamo una situazione semplice in cui vogliamo associare dei dati extra a oggetti DOM:
const cache = new Map();
function registra(elemento) {
cache.set(elemento, {
creato: Date.now()
});
}
A prima vista non c'è nulla di strano. Il problema emerge quando elemento
Questo è un classico esempio di memory leak silenzioso. Il codice funziona, ma la memoria cresce nel tempo.
Come cambia tutto con WeakMap
WeakMap nasce per risolvere questo tipo di problema. A differenza di Map, le chiavi di una WeakMap sono deboli. Questo significa che se non esistono altri riferimenti all'oggetto chiave, il garbage collector è libero di eliminarlo, insieme al valore associato. Rivediamo l'esempio precedente:
const cache = new WeakMap();
function registra(elemento) {
cache.set(elemento, {
creato: Date.now()
});
}
Ora il comportamento cambia radicalmente. Se elemento
Questo rende WeakMap ideale per associare dati a oggetti senza controllarne direttamente il ciclo di vita. Ma perché WeakMap non è una Map "normale"? È importante capire che WeakMap non è semplicemente una versione "più leggera" di Map. Ha limitazioni precise che derivano proprio dal suo funzionamento.
Non è possibile iterare su una WeakMap. Non si possono ottenere tutte le chiavi o tutti i valori. Questo non è un difetto, ma una conseguenza logica: se le chiavi possono sparire in qualsiasi momento per effetto del garbage collector, non avrebbe senso offrire un'iterazione stabile.
Allo stesso modo, le chiavi devono essere oggetti. Non è possibile usare stringhe o numeri, perché i valori primitivi non hanno un ciclo di vita gestito dal garbage collector nello stesso modo.
Queste caratteristiche rendono WeakMap meno flessibile in apparenza, ma molto più sicura in contesti specifici.
WeakSet: il caso dei flag temporanei
In JavaScript WeakSet segue lo stesso principio, ma invece di associare chiavi e valori, mantiene semplicemente un insieme di oggetti. Anche qui i riferimenti sono deboli.
Un caso d'uso molto interessante riguarda i flag temporanei. Immaginiamo di voler segnare alcuni oggetti come "già processati", senza alterarne la struttura e senza rischiare di creare leak. Con un Set tradizionale:
const processati = new Set();
function processa(oggetto) {
if (processati.has(oggetto)) return;
processati.add(oggetto);
console.log("Processo", oggetto);
}
Anche qui, se oggetto non viene più usato altrove, il Set continuerà a mantenerlo in memoria.
Con WeakSet:
const processati = new WeakSet();
function processa(oggetto) {
if (processati.has(oggetto)) return;
processati.add(oggetto);
console.log("Processo", oggetto);
}
Ora il flag è "trasparente" rispetto alla memoria. Se l'oggetto non è più utilizzato, verrà comunque eliminato.
Evitare leak con listener e metadati in JavaScript
Uno scenario molto concreto riguarda l'associazione di metadati a oggetti che hanno una vita limitata, come elementi DOM o istanze di classi. Immaginiamo di voler tracciare informazioni su elementi a cui aggiungiamo un event listener:
const metadata = new Map();
function attach(el) {
const handler = () => console.log("click");
el.addEventListener("click", handler);
metadata.set(el, { handler });
}
Se ci dimentichiamo di rimuovere manualmente tutto, la Map continuerà a mantenere riferimenti agli elementi e ai loro handler.
Usando WeakMap:
const metadata = new WeakMap();
function attach(el) {
const handler = () => console.log("click");
el.addEventListener("click", handler);
metadata.set(el, { handler });
}
In questo caso, se l'elemento viene rimosso e non è più referenziato, anche il metadato associato non impedirà la sua raccolta. Questo non elimina la necessità di pulire i listener quando serve, ma riduce drasticamente il rischio di leak accidentali.
Caching intelligente senza trattenere oggetti inutili
Un altro utilizzo molto interessante di WeakMap riguarda il caching. In alcune situazioni vogliamo memorizzare risultati legati a oggetti, ma senza obbligarli a restare in memoria. Un esempio semplice:
const cache = new WeakMap();
function calcola(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}
const risultato = {
valore: Math.random()
};
cache.set(obj, risultato);
return risultato;
}
Qui il risultato è memorizzato finché l'oggetto obj
obj
WeakMap e incapsulamento dei dati
Un uso meno evidente ma molto elegante di WeakMap riguarda l'incapsulamento. Prima dell'introduzione dei campi privati nelle classi, WeakMap veniva spesso utilizzata per simulare proprietà private. Anche oggi rimane una tecnica interessante in alcuni contesti:
const privati = new WeakMap();
class Contatore {
constructor() {
privati.set(this, { valore: 0 });
}
incrementa() {
const stato = privati.get(this);
stato.valore++;
}
getValore() {
return privati.get(this).valore;
}
}
Qui i dati interni non sono accessibili dall'esterno. Inoltre, quando l'istanza viene eliminata, anche i dati associati nella WeakMap possono essere raccolti.
Quando usare WeakMap e WeakSet in JavaScript
Nonostante i vantaggi, è importante non abusare di queste strutture. Non sono un sostituto universale di Map e Set. Il loro utilizzo ha senso quando il ciclo di vita dei dati deve seguire quello degli oggetti a cui sono associati.
Se serve iterare, contare elementi o mantenere uno stato persistente indipendente dagli oggetti, allora Map e Set restano la scelta corretta.
Se invece si lavora con metadati, cache legate a oggetti, flag temporanei o associazioni che non devono impedire la garbage collection, allora WeakMap e WeakSet diventano strumenti fondamentali.
Perché queste strutture aiutano davvero a evitare leak? Il punto è che WeakMap e WeakSet permettono di allineare la struttura dei dati con il comportamento del garbage collector. Invece di creare riferimenti forti che mantengono in vita oggetti inutili, si creano associazioni che esistono solo finché servono davvero.
Questo non elimina completamente il problema dei memory leak. Errori logici, listener non rimossi, closure persistenti o riferimenti globali possono comunque causare problemi. Ma riduce in modo significativo una classe molto comune di leak legati a strutture dati "invisibili".
Una mentalità diversa nella gestione della memoria
Usare WeakMap e WeakSet significa anche cambiare prospettiva. Si tratta di ragionare sul ciclo di vita degli oggetti in JavaScript. Per quanto tempo devono esistere? Se la risposta è "finché esiste questo oggetto", probabilmente allora una struttura debole è la scelta giusta.