WeakMap und WeakSet – Speicher effizient verwalten
JavaScript bietet seit ES6 bereits mächtige Datenstrukturen wie Map
und Set
, um Schlüssel-Wert-Paare oder Mengen von Werten effizient abzubilden. Doch manchmal braucht man Strukturen, die Objekte als Schlüssel oder Werte speichern, ohne den Garbage Collector an der Freigabe ungenutzter Objekte zu hindern. Hier kommen WeakMap und WeakSet ins Spiel. Diese Datenstrukturen erlauben es, Objekte “schwach” zu referenzieren. Das bedeutet, dass ein Objekt, auf das nur über eine WeakMap oder ein WeakSet zugegriffen wird, vom Garbage Collector aufgeräumt werden kann, sobald es sonst keine Verweise mehr darauf gibt.
Was sind WeakMap und WeakSet?
-
WeakMap ist ähnlich einer
Map
, unterscheidet sich aber darin, dass ihre Schlüssel ausschließlich Objekte sein müssen und diese nur “schwach” referenziert werden. Wird ein Objekt außerhalb der WeakMap nicht mehr genutzt, kann der Garbage Collector es freigeben, ohne dass die WeakMap es daran hindern würde. -
WeakSet funktioniert analog zu einer
Set
, erlaubt jedoch ausschließlich Objektwerte und hält diese ebenfalls nur schwach. Sobald ein Objekt außerhalb des WeakSets nicht mehr referenziert wird, kann es vom Garbage Collector entfernt werden.
Diese schwachen Referenzen verhindern Memory-Leaks, wenn man Objekte nur für bestimmte Zwecke (z. B. zur internen Markierung, Caching oder zur Zugriffskontrolle) vorübergehend in einer Datenstruktur halten möchte, ohne langfristig den Speicher zu blockieren.
Wichtige Eigenschaften
-
Keine Enumeration:
Anders als bei Map oder Set gibt es bei WeakMap und WeakSet keine Methoden, um alle enthaltenen Schlüssel oder Werte auszulesen (no keys(), values(), entries()
-Methoden). Das liegt daran, dass sich der Inhalt dynamisch ändern kann, wenn der Garbage Collector Objekte entfernt. -
Schlüssel und Werte müssen Objekte sein:
WeakMap-Schlüssel und WeakSet-Elemente müssen vom Typ Objekt sein. Primitive Werte (String, Number, Boolean etc.) sind nicht erlaubt. -
Automatische Speicherfreigabe:
Wenn kein anderer Verweis mehr auf ein Objekt existiert, verschwindet es im Hintergrund aus der WeakMap bzw. dem WeakSet, ohne dass du etwas tun musst.
Einfache Beispiele
WeakMap-Beispiel:
const wm = new WeakMap();
let obj = { name: 'Max' };
wm.set(obj, 'Daten für Max');
console.log(wm.get(obj)); // 'Daten für Max'
// Wenn wir nun die einzige Referenz auf obj entfernen:
obj = null;
// Ab hier kann der Garbage Collector das ehemals von obj referenzierte Objekt freigeben.
// wm.get(obj) wäre nicht mehr möglich, da obj = null und somit nicht mehr vorhanden ist.
console.log(wm.get(obj)); // undefined
In diesem Beispiel wird das Objekt obj
als Schlüssel in der WeakMap verwendet. Nach dem Setzen von obj = null
existiert kein weiterer Verweis mehr auf das ursprüngliche Objekt. Der Garbage Collector kann es freigeben, und die WeakMap hindert ihn nicht daran.
WeakSet-Beispiel:
const ws = new WeakSet();
let userObj = { id: 12345 };
ws.add(userObj);
console.log(ws.has(userObj)); // true
// Entfernen der letzten Referenz
userObj = null;
// Das Objekt kann nun von der WeakSet-Datenstruktur "vergessen" werden,
// sobald der Garbage Collector läuft.
console.log(ws.has(userObj)); // false
Hier wird userObj
in einem WeakSet gespeichert. Sobald userObj
auf null
gesetzt wird, gibt es keinen Verweis mehr auf das eigentliche Objekt, und es kann aus dem Speicher entfernt werden. Der Entwickler muss sich nicht aktiv um das Entfernen aus dem WeakSet kümmern.
Wie unterscheiden sich WeakMap und WeakSet?
Der wesentliche Unterschied zwischen WeakMaps und WeakSets liegt darin, was man in ihnen speichert und wie man sie nutzt:
-
WeakMap:
- Eine WeakMap speichert Schlüssel-Wert-Paare, wobei die Schlüssel ausschließlich Objekte sein müssen.
- Du kannst also für jedes Objekt zusätzliche Daten hinterlegen, ohne diese direkt am Objekt selbst speichern zu müssen.
- Der häufigste praktische Anwendungsfall ist es, einem Objekt Metadaten oder Konfigurationen zuzuordnen, ohne das Originalobjekt zu verändern. Wenn das Objekt nicht mehr referenziert wird, verschwindet es und damit auch die zugehörigen Metadaten aus der WeakMap.
Beispiel:
const metadataFor = new WeakMap(); function setMetadata(obj, data) { metadataFor.set(obj, data); } function getMetadata(obj) { return metadataFor.get(obj); } const user = { name: 'Anna' }; setMetadata(user, { lastLogin: Date.now() }); console.log(getMetadata(user)); // { lastLogin: 1673456789012 } // Wird user = null gesetzt und GC läuft, verschwindet auch dieser Eintrag.
In diesem Codebeispiel wird eine WeakMap namens metadataFor erstellt, um Metadaten für Objekte zu speichern. Desweiteren wird hier eine setter und eine getter Funktion definiert.
Die Funktion setMetadata nimmt ein Objekt obj und ein Datenobjekt data entgegen und speichert die Daten in der WeakMap unter dem Schlüssel obj. Die Funktion getMetadata nimmt ein Objekt obj entgegen und gibt die zugehörigen Daten aus der WeakMap zurück.
Ein Benutzerobjekt user wird erstellt, das den Namen Anna enthält. Die Funktion setMetadata wird aufgerufen, um Metadaten für das Benutzerobjekt zu setzen. In diesem Fall wird das aktuelle Datum und die aktuelle Uhrzeit als lastLogin gespeichert.
Die Funktion getMetadata wird aufgerufen, um die Metadaten für das Benutzerobjekt abzurufen und in der Konsole auszugeben. Die Ausgabe zeigt die gespeicherten Metadaten, z.B. { lastLogin: 1673456789012 }.
Ein wichtiger Aspekt der WeakMap ist, dass die Einträge automatisch entfernt werden, wenn die Schlüsselobjekte nicht mehr referenziert werden. Wenn das Benutzerobjekt user auf null gesetzt wird und der Garbage Collector läuft, wird der Eintrag für user aus der WeakMap entfernt.
Dieses Beispiel zeigt, wie WeakMap verwendet werden kann, um Metadaten für Objekte zu speichern, ohne das Risiko von Speicherlecks, da die Einträge automatisch entfernt werden, wenn die Schlüsselobjekte nicht mehr benötigt werden.
Kurz gesagt: Verwende eine WeakMap, wenn du einem Objekt zusätzliche Informationen zuordnen möchtest, die komplexer als nur ein Flag sind.
-
WeakSet:
- Ein WeakSet speichert nur Objekte, ohne ihnen Werte zuzuweisen.
- Du kannst damit also Objekte lediglich “markieren” oder kategorisieren, ohne weitere Daten zu hinterlegen.
- Sobald das Objekt nicht mehr referenziert wird, verschwindet auch seine “Markierung” aus dem WeakSet.
Beispiel:
const processed = new WeakSet(); function processElement(el) { if (!processed.has(el)) { // Hier wird das Element das erste Mal verarbeitet el.style.backgroundColor = 'lightgreen'; processed.add(el); } else { console.log('Element wurde bereits verarbeitet.'); } } const div = document.createElement('div'); processElement(div); // Element wird verarbeitet processElement(div); // Meldung: bereits verarbeitet // Entfernt man div aus dem DOM und verliert alle Referenzen darauf, // verschwindet es auch aus dem WeakSet.
In diesem Codebeispiel wird ein WeakSet namens processed erstellt, um DOM-Elemente zu verfolgen, die bereits verarbeitet wurden. Zur Verarbeitung dessen wird eine Funktion namens processElement erstellt.
Die Funktion processElement nimmt ein DOM-Element el entgegen und überprüft, ob dieses Element bereits im WeakSet processed enthalten ist. Wenn das Element noch nicht verarbeitet wurde (!processed.has(el)), wird es verarbeitet, indem seine Hintergrundfarbe auf ‘lightgreen’ gesetzt wird, und das Element wird dem WeakSet hinzugefügt. Wenn das Element bereits verarbeitet wurde, wird eine Meldung in der Konsole ausgegeben.
Ein div-Element wird mit document.createElement(‘div’) erstellt und zweimal an die Funktion processElement übergeben. Beim ersten Aufruf wird das Element verarbeitet und seine Hintergrundfarbe geändert. Beim zweiten Aufruf wird die Meldung “Element wurde bereits verarbeitet.” ausgegeben, da das Element bereits im WeakSet enthalten ist.
Ein wichtiger Aspekt des WeakSet ist, dass die Einträge automatisch entfernt werden, wenn die Elemente nicht mehr referenziert werden. Wenn das div-Element aus dem DOM entfernt wird und alle Referenzen darauf verloren gehen, wird es auch aus dem WeakSet entfernt.
Dieses Beispiel zeigt, wie WeakSet verwendet werden kann, um zu verfolgen, welche DOM-Elemente bereits verarbeitet wurden, ohne das Risiko von Speicherlecks, da die Einträge automatisch entfernt werden, wenn die Elemente nicht mehr benötigt werden.
Kurz gesagt: Verwende ein WeakSet, wenn du nur wissen willst, ob ein Objekt bereits verarbeitet oder “markiert” wurde, ohne dabei zusätzliche Daten speichern zu müssen.
Der Unterschied in der Praxis:
- WeakMap: Denk an ein verstecktes Notizbuch, in dem du für bestimmte Objekte (z. B. Nutzer, DOM-Elemente) zusätzliche Informationen niederschreibst, ohne das Objekt selbst zu verändern.
- WeakSet: Stell dir einen Stempel vor: Du stempelst ein Objekt als “verarbeitet”. Es gibt keine weiteren Informationen außer “ist verarbeitet” oder “ist es nicht”.
Beide Strukturen teilen die Eigenschaft, dass sie den Garbage Collector nicht daran hindern, die Objekte freizugeben, sobald diese nicht mehr in Verwendung sind. Der Einsatz einer WeakMap oder eines WeakSet hängt letztendlich davon ab, ob du zu jedem Objekt Daten speichern möchtest (WeakMap) oder es dir nur um ein einfaches “Flag” (WeakSet) geht.
Mehr praktische Beispiele für die Nutzung von WeakMaps
-
Caches für Objekte:
Möchtest du beispielsweise Ergebnisse von teuren Berechnungen für bestimmte Objekte cachen, aber sicherstellen, dass der Cache keine Memory-Leaks verursacht, wenn die Objekte nicht mehr verwendet werden, ist eine WeakMap ideal.const computeSomethingExpensive = (obj) => obj; const cache = new WeakMap(); function heavyComputation(obj) { if (cache.has(obj)) { return cache.get(obj); } const result = computeSomethingExpensive(obj); cache.set(obj, result); return result; } let dataObj = { payload: 'große Daten' }; console.log(heavyComputation(dataObj)); // Berechnung und Cache-Eintrag console.log(heavyComputation(dataObj)); // Direkt aus Cache // Wenn dataObj irgendwann nicht mehr benötigt wird: dataObj = null; // Der Cache kann das zugehörige Ergebnis freigeben, wenn GC läuft. console.log(heavyComputation(dataObj)); // TypeError: Invalid value used as weak map key
-
Metadaten für Objekte:
Stell dir vor, du möchtest bestimmte Objekte “markieren” oder ihnen Metadaten zuordnen, ohne ihr ursprüngliches Objekt zu verändern. Eine WeakMap kann hier als “Nebenregister” für zusätzliche Informationen dienen, ohne dass du riskierst, dass diese Metadaten ewig im Speicher bleiben.// WeakMap zur Zuordnung von Metadaten zu Objekten const metadataStore = new WeakMap(); function setMetadata(obj, meta) { metadataStore.set(obj, meta); } function getMetadata(obj) { return metadataStore.get(obj); } // Beispielobjekte const user = { name: 'Anna' }; const documentObj = { title: 'Mein Dokument' }; // Metadaten zuordnen, ohne die Originalobjekte zu verändern setMetadata(user, { lastLogin: new Date().toString() }); setMetadata(documentObj, { confidential: true }); console.log(getMetadata(user)); // { lastLogin: lastLogin: "Tue Dec 17 2024 07:30:54 GMT+0100 (Mitteleuropäische Normalzeit)" } (Beispieldatum) console.log(getMetadata(documentObj)); // { confidential: true } // Wenn jetzt "user" nicht mehr referenziert wird (z. B. user = null;), // kann das Objekt und dessen Metadaten vom Garbage Collector freigegeben werden, // da WeakMap keine starke Referenz auf das Objekt hält.
-
DOM-Referenzen:
Häufig wird das DOM dynamisch modifiziert. Mit einer WeakMap kann man einem DOM-Element zusätzliche Daten zuordnen, ohne dass dieser Eintrag das Element künstlich im Speicher hält, wenn es aus dem DOM entfernt wird.const elementData = new WeakMap(); function storeDataForElement(el, data) { elementData.set(el, data); } function getDataForElement(el) { return elementData.get(el); } const div = document.createElement('div'); storeDataForElement(div, { clicked: false }); // Irgendwann entfernst du div aus dem DOM: document.body.appendChild(div); // ... später ... document.body.removeChild(div); // Da nun kein Verweis mehr auf div existiert, kann der GC das Element entfernen // und damit auch den Dateneintrag in elementData.
Mehr praktische Beispiele für die Nutzung von WeakSets
-
DOM-Elemente markieren:
Stelle dir vor, du möchtest wissen, welche DOM-Elemente bereits bearbeitet wurden. Durch die Verwendung eines WeakSets bleiben keine unnötigen Speicherlecks zurück.const processedElements = new WeakSet(); function processElement(el) { if (!processedElements.has(el)) { // Führe einmalige Verarbeitung durch, z. B. Stil anpassen el.style.backgroundColor = 'lightgreen'; processedElements.add(el); } else { console.log('Dieses Element wurde bereits verarbeitet.'); } } // Beispielhafte Verwendung const div = document.createElement('div'); document.body.appendChild(div); processElement(div); // Element wird verarbeitet processElement(div); // Meldung: Bereits verarbeitet // Wenn div aus dem DOM entfernt und nirgends mehr referenziert wird, // kann es aus dem WeakSet "verschwinden". document.body.removeChild(div);
-
Verarbeitete Objekte in einer Pipeline kennzeichnen:
Angenommen, du bekommst aus einer Datenpipeline immer wieder neue Objekte, und du möchtest sicherstellen, dass jedes Objekt nur einmalig verarbeitet wird. Ein WeakSet verhindert, dass du Objekte ständig im Gedächtnis halten musst, wenn sie nicht mehr referenziert werden.const handledItems = new WeakSet(); function handleItem(item) { if (handledItems.has(item)) { console.log('Item wurde bereits verarbeitet.'); return; } // Aufwendige Verarbeitung console.log('Verarbeite Item:', item); handledItems.add(item); } // Beispiel: Datenobjekte aus einem Stream let itemA = { id: 1, data: 'abc' }; let itemB = { id: 2, data: 'xyz' }; handleItem(itemA); // "Verarbeite Item: { id: 1, data: 'abc' }" handleItem(itemA); // "Item wurde bereits verarbeitet." handleItem(itemB); // "Verarbeite Item: { id: 2, data: 'xyz' }" // Werden die Variablen nicht mehr benötigt (z. B. itemA = null;), // kann das Objekt aus dem WeakSet verschwinden. itemA = null; itemB = null;
Hier sorgt das WeakSet dafür, dass einmal behandelte Objekte nicht versehentlich doppelt verarbeitet werden, aber auch kein unnötiger Speicher belegt bleibt, nachdem die Objekte frei gegeben wurden.
Einschränkungen
-
Keine Iteration:
Du kannst nicht einfach über alle Elemente in einer WeakMap oder einem WeakSet iterieren. Das liegt an deren flüchtiger Natur: Der Inhalt kann sich jederzeit ändern, wenn Objekte freigegeben werden. -
Fehler bei Primitiven:
Du musst sicherstellen, dass du nur Objekte als Schlüssel/Elemente hinzufügst. Der Versuch, einen primitiven Wert hinzuzufügen, führt zu einem Fehler. -
Nicht geeignet für dauerhafte Datenstrukturen:
Da du die Inhalte nicht iterieren kannst und sie im Hintergrund verschwinden, sind WeakMap und WeakSet nicht für dauerhafte Datenhaltung oder umfangreiche Datenstrukturen geeignet. Sie dienen eher als Ergänzung, um temporäre Beziehungen zwischen Objekten herzustellen.
Fazit
WeakMap und WeakSet sind spezialisierte Datenstrukturen, die dabei helfen, Speicher effizienter zu verwalten, indem sie schwache Referenzen auf Objekte halten. Sie sind besonders nützlich, wenn man Metadaten oder Caches an bestimmte Objekte knüpfen möchte, ohne Memory-Leaks zu riskieren. Für allgemeine Aufgaben, bei denen man Daten strukturiert vorhalten, durchsuchen und iterieren möchte, eignen sich Map
und Set
besser. Doch wenn es um temporäre Verknüpfungen und automatische Speicherfreigabe geht, sind WeakMap und WeakSet unersetzliche Werkzeuge im JavaScript-Werkzeugkasten.
Probiere WeakMaps und WeakSets in deinem nächsten Projekt aus, um Schlüssel-Wert-Paare oder Mengen von Werten effizient abzubilden und mögliche Memory-Leaks zu vermeiden!