Skip to content

Türchen 20

Published: at 05:00 AMSuggest Changes

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?

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

  1. 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.

  2. 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.

  3. 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:

  1. 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.

  2. 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:

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

  1. 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
    
  2. 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.
    
  3. 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

  1. 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);
    
  2. 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

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!


Previous Post
Türchen 21
Next Post
Türchen 19