Private Object-Zustände ohne Memory Leaks
Ein gewöhnliches Map-Objekt als Zuordnung von Objekten zu privatem Zustand verhindert, dass diese Objekte jemals vom Garbage Collector eingesammelt werden, solange die Map existiert. WeakMap und WeakSet lösen genau dieses Problem, indem sie ihre Schlüssel nur schwach referenzieren und damit private Zustände ermöglichen, die automatisch mit dem zugehörigen Objekt verschwinden.
Inhaltsverzeichnis
- 1. Das Memory-Leak-Problem gewöhnlicher Map-Zuordnungen
- 2. WeakMap und WeakSet: schwache Referenzen erklärt
- 3. Einschränkungen: keine Iteration, kein size
- 4. Praxisfall: Private Felder vor Klassen-Syntax
- 5. DOM-Metadaten ohne Speicherlecks verwalten
- 6. Caches mit automatischer Bereinigung über WeakMap
- 7. WeakSet: Markierung statt Zuordnung
- 8. WeakMap vs. private Klassenfelder mit #
- 9. WeakMap/WeakSet im direkten Vergleich zu Map/Set
- 10. Zusammenfassung
- 11. FAQ
1. Das Memory-Leak-Problem gewöhnlicher Map-Zuordnungen
Braucht man in JavaScript zusätzlichen Zustand zu einem bestehenden Objekt, ohne dieses Objekt selbst zu verändern, liegt eine gewöhnliche Map nahe: das Objekt wird als Schlüssel verwendet, der zusätzliche Zustand als Wert. Das Problem: eine Map hält starke Referenzen auf ihre Schlüssel. Solange die Map existiert, verhindert sie, dass der Garbage Collector das als Schlüssel verwendete Objekt jemals einsammelt, selbst wenn nirgendwo sonst im Programm noch eine Referenz darauf existiert.
WeakMap und WeakSet lösen dieses Problem strukturell. Beide Datenstrukturen halten ihre Schlüssel beziehungsweise Elemente nur über schwache Referenzen. Eine schwache Referenz verhindert die Garbage Collection des referenzierten Objekts nicht. Sobald keine starke Referenz mehr auf ein Objekt existiert, das als Schlüssel in einer WeakMap verwendet wurde, wird das Objekt eingesammelt, und der zugehörige Eintrag in der WeakMap verschwindet automatisch mit.
2. WeakMap und WeakSet: schwache Referenzen erklärt
Eine WeakMap funktioniert von der Aufrufsyntax her fast identisch zu einer gewöhnlichen Map: set, get, has und delete stehen zur Verfügung. Der entscheidende Unterschied liegt in der Einschränkung, dass nur Objekte als Schlüssel zulässig sind, keine primitiven Werte. Diese Einschränkung ist kein Versehen, sondern folgt direkt aus dem Zweck der Datenstruktur: primitive Werte werden von der JavaScript-Engine ohnehin nicht durch Referenzen verwaltet, das Konzept der schwachen Referenz ergibt für sie keinen Sinn.
Eine WeakSet speichert analog dazu ausschließlich Objekte als Elemente, wieder über schwache Referenzen. Anders als bei einer Map mit Schlüssel-Wert-Paaren dient eine WeakSet primär dazu, Objekte zu markieren, etwa "dieses Objekt wurde bereits verarbeitet", ohne selbst zusätzliche Werte zu speichern. Beide Strukturen wurden mit ES2015 eingeführt und sind in allen modernen JavaScript-Engines vollständig implementiert.
const metadata = new WeakMap();
function attachMetadata(obj, info) {
// The key (obj) is only weakly referenced by the WeakMap
metadata.set(obj, info);
}
let user = { name: "Alice" };
attachMetadata(user, { role: "admin", lastSeen: Date.now() });
console.log(metadata.get(user)); // { role: "admin", lastSeen: ... }
user = null; // No more strong references to the original object
// The WeakMap entry can now be garbage collected along with the key
3. Einschränkungen: keine Iteration, kein size
Die schwache Referenzierung, die WeakMap und WeakSet so nützlich macht, bringt eine bewusste Einschränkung mit sich: keine der beiden Strukturen ist iterierbar. Es gibt keine keys(), values(), entries() oder forEach-Methode, und auch keine size-Eigenschaft. Der Grund ist technischer Natur: der genaue Zeitpunkt der Garbage Collection ist in JavaScript nicht deterministisch und liegt in der Kontrolle der Engine, nicht des Programmierers.
Würde man über eine WeakMap iterieren können, wäre das Ergebnis der Iteration vom exakten, nicht vorhersagbaren Zeitpunkt der Garbage Collection abhängig, was zu nicht reproduzierbarem Programmverhalten führen würde. Diese Einschränkung ist also kein fehlendes Feature, sondern eine bewusste Design-Entscheidung, die WeakMap und WeakSet ausschließlich für Anwendungsfälle geeignet macht, in denen man einen bekannten Schlüssel gezielt nachschlägt, nicht für Fälle, in denen man alle gespeicherten Einträge auflisten muss.
4. Praxisfall: Private Felder vor Klassen-Syntax
Bevor JavaScript mit #-Syntax native private Klassenfelder bekam, war WeakMap das Standard-Pattern, um wirklich privaten Zustand für Instanzen einer Klasse zu implementieren. Der Zustand wurde in einer modul-weiten WeakMap außerhalb der Klasse gespeichert, mit der jeweiligen Instanz als Schlüssel. Von außerhalb des Moduls war dieser Zustand nicht zugreifbar, weil die WeakMap selbst nicht exportiert wurde, und im Gegensatz zu Konventionen wie einem Unterstrich-Präfix war der Zustand wirklich, nicht nur konventionell privat.
Dieses Pattern hat auch heute noch praktische Relevanz, etwa wenn privater Zustand über mehrere zusammengehörige Klassen hinweg geteilt werden muss, ein Anwendungsfall, den native private Felder mit # nicht direkt unterstützen, weil sie strikt an eine einzelne Klasse gebunden sind.
// Module-level WeakMap holds truly private state, keyed by instance
const privateState = new WeakMap();
class BankAccount {
constructor(initialBalance) {
// The balance is not accessible from outside this module at all
privateState.set(this, { balance: initialBalance });
}
deposit(amount) {
const state = privateState.get(this);
state.balance += amount;
}
getBalance() {
return privateState.get(this).balance;
}
}
const account = new BankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
console.log(account.balance); // undefined — truly private
5. DOM-Metadaten ohne Speicherlecks verwalten
Ein klassischer Anwendungsfall für WeakMap ist das Anhängen zusätzlicher Metadaten an DOM-Elemente, ohne diese Elemente selbst zu verändern und ohne eigene Datenattribute zu missbrauchen. Wird ein DOM-Element irgendwann aus dem Dokument entfernt und existiert keine weitere JavaScript-Referenz darauf mehr, kann der Garbage Collector sowohl das Element als auch die zugehörigen Metadaten in der WeakMap gleichzeitig einsammeln.
Mit einer gewöhnlichen Map wäre das nicht der Fall: entfernt man ein Element aus dem DOM, aber die Map hält weiterhin eine starke Referenz darauf, bleibt das Element im Speicher, obwohl es für die Anwendung längst irrelevant geworden ist. Bei Single-Page-Applications mit häufigem DOM-Auf- und -Abbau summiert sich dieser Effekt über die Zeit zu spürbaren Speicherlecks, die WeakMap von vornherein ausschließt.
const elementState = new WeakMap();
function trackClicks(el) {
elementState.set(el, { clickCount: 0 });
el.addEventListener("click", () => {
const state = elementState.get(el);
state.clickCount++;
console.log(`Clicked ${state.clickCount} times`);
});
}
const button = document.querySelector("#my-button");
trackClicks(button);
// If #my-button is later removed from the DOM and dereferenced,
// its entry in elementState is eligible for garbage collection too
6. Caches mit automatischer Bereinigung über WeakMap
Ein weiterer praktischer Anwendungsfall ist das Caching von Berechnungsergebnissen, die von einem Objekt abhängen. Statt das Ergebnis in einer gewöhnlichen Map zwischenzuspeichern und selbst dafür zu sorgen, dass veraltete Einträge entfernt werden, übernimmt WeakMap diese Bereinigung automatisch: sobald das Quellobjekt nicht mehr referenziert wird, verschwindet auch der zugehörige Cache-Eintrag, ohne dass explizite Aufräumlogik notwendig wäre.
Das ist besonders wertvoll bei teuren Berechnungen, deren Ergebnis an ein temporäres Objekt gebunden ist, etwa geparste Konfigurationsdaten oder berechnete Layout-Informationen zu einem UI-Komponenten-Objekt. Der Cache wächst nie unbegrenzt, weil er sich selbst über die Lebensdauer der referenzierten Objekte begrenzt, was bei einer gewöhnlichen Map nur mit zusätzlicher, manuell gepflegter Logik möglich wäre.
7. WeakSet: Markierung statt Zuordnung
WeakSet wird seltener eingesetzt als WeakMap, hat aber einen klaren eigenen Anwendungsfall: das Markieren von Objekten, ohne zusätzliche Daten daran zu speichern. Ein typisches Beispiel ist die Nachverfolgung, ob ein Objekt bereits verarbeitet wurde, etwa in einem rekursiven Algorithmus, der zirkuläre Referenzen erkennen muss. Statt eine Map mit dem Wert true für jeden Schlüssel zu missbrauchen, drückt eine WeakSet die reine Mitgliedschaft klarer aus.
Genau dieses Muster kommt auch bei einer robusten Deep-Equality-Implementierung zum Einsatz, wo eine WeakSet oder WeakMap bereits besuchte Objektpaare während einer rekursiven Traversierung markiert, um Endlosschleifen bei zirkulären Referenzen zu vermeiden. Auch hier gilt: sobald die traversierten Objekte nach Abschluss der Operation nicht mehr referenziert werden, gibt der Garbage Collector den Speicher der Markierungen automatisch frei.
const processed = new WeakSet();
function processOnce(obj) {
if (processed.has(obj)) {
console.log("Already processed, skipping");
return;
}
processed.add(obj);
console.log("Processing for the first time");
// ... actual processing logic here
}
const item = { id: 1 };
processOnce(item); // Processing for the first time
processOnce(item); // Already processed, skipping
8. WeakMap vs. private Klassenfelder mit #
Seit private Klassenfelder mit #name-Syntax nativ in JavaScript verfügbar sind, ist der ursprüngliche Hauptanwendungsfall von WeakMap, wirklich privater Klassenzustand, für die meisten neuen Klassen weniger relevant geworden. Private Felder sind einfacher zu lesen, benötigen keine externe Datenstruktur und werden von der Engine direkt unterstützt, inklusive besserer Fehlermeldungen bei falscher Verwendung.
WeakMap bleibt aber überlegen, wenn privater Zustand zwischen mehreren, nicht in derselben Klassenhierarchie stehenden Objekten geteilt werden muss, oder wenn Zustand nachträglich zu bereits existierenden Objekten hinzugefügt werden soll, die man selbst nicht kontrolliert, etwa DOM-Elemente oder Objekte aus einer Drittanbieter-Bibliothek. Für neue, selbst geschriebene Klassen sind private Felder meist die einfachere Wahl, für Metadaten zu fremden Objekten bleibt WeakMap das richtige Werkzeug.
9. WeakMap/WeakSet im direkten Vergleich zu Map/Set
Die folgende Tabelle stellt die zentralen Unterschiede zwischen den schwachen und den gewöhnlichen Sammlungstypen gegenüber und zeigt, wann welche Variante die richtige Wahl ist.
| Eigenschaft | Map / Set | WeakMap / WeakSet | Konsequenz |
|---|---|---|---|
| Referenztyp auf Schlüssel | Stark | Schwach | WeakMap verhindert keine Garbage Collection |
| Erlaubte Schlüssel | Beliebige Werte | Nur Objekte | Primitive brauchen normale Map |
| Iterierbar | Ja | Nein | Kein forEach, keys, size bei Weak-Varianten |
| Speicherverhalten | Kann Leaks verursachen | Selbstbereinigend | WeakMap für DOM-Metadaten und Caches geeignet |
| Typischer Einsatz | Zähler, geordnete Sammlungen | Privater Zustand, Metadaten, Markierung | Anwendungsfall entscheidet |
Die Wahl zwischen beiden Familien ist selten eine reine Performance-Frage, sondern eine Frage der Lebensdauer-Semantik: soll der zusätzliche Zustand exakt an die Lebensdauer eines Objekts gekoppelt sein, ist WeakMap die richtige Wahl. Muss man alle gespeicherten Einträge auflisten oder zählen können, bleibt eine gewöhnliche Map die einzige Option.
Mironsoft
Speicherverwaltung, Frontend-Architektur und Performance
Anwendungen, die auch nach Stunden Laufzeit schlank bleiben?
Wir finden Speicherlecks in bestehenden Single-Page-Applications, ersetzen problematische Map-basierte Metadaten-Zuordnungen durch WeakMap-Patterns und richten private Zustandsverwaltung so ein, dass sie sich automatisch selbst bereinigt.
Memory-Profiling
Speicherlecks in DOM-Metadaten und Zustandsverwaltung aufspüren
Code-Modernisierung
Map-basierte Muster durch WeakMap und private Klassenfelder ersetzen
Architektur-Beratung
Cache-Strategien mit automatischer Speicherbereinigung entwerfen
10. Zusammenfassung
WeakMap und WeakSet lösen ein reales Speicherproblem, das mit gewöhnlichen Map- und Set-Objekten entsteht, wenn Objekte als Schlüssel dienen: die schwache Referenzierung sorgt dafür, dass Objekte weiterhin normal vom Garbage Collector eingesammelt werden können, sobald keine starken Referenzen mehr auf sie existieren. Die bewusste Einschränkung auf Nicht-Iterierbarkeit ist eine direkte Folge davon, dass der Zeitpunkt der Garbage Collection nicht deterministisch ist.
Die praktischen Haupteinsatzgebiete sind privater Klassenzustand vor der #-Syntax, Metadaten zu DOM-Elementen ohne Speicherlecks, selbstbereinigende Caches und die Markierung bereits besuchter Objekte in rekursiven Algorithmen. Für neue, selbst kontrollierte Klassen sind native private Felder oft die einfachere Wahl, für Zustand zu fremden oder dynamisch erzeugten Objekten bleiben WeakMap und WeakSet das richtige Werkzeug.
WeakMap und WeakSet — Das Wichtigste auf einen Blick
Grundprinzip
Schwache Referenzen auf Objekt-Schlüssel verhindern keine Garbage Collection, anders als bei Map/Set.
Einschränkungen
Nicht iterierbar, keine size-Eigenschaft, nur Objekte als Schlüssel zulässig.
Typische Einsatzgebiete
Privater Klassenzustand, DOM-Metadaten, selbstbereinigende Caches, Markierung besuchter Objekte.
Abgrenzung zu # Feldern
Private Felder für eigene Klassen, WeakMap für Zustand zu fremden oder externen Objekten.