Start · Sprachen · JavaScript · Referenz · WeakMap

WeakMap

Klasse

Eine Sammlung von Schlüssel/Wert-Paaren mit Objekten oder nicht-registrierten Symbols als Schlüsseln, die keine starken Referenzen auf ihre Schlüssel hält.

seit JavaScript ES2015 Kategorie: core

Signatur

new WeakMap() new WeakMap(iterable)

Beschreibung

Ein WeakMap ist eine Sammlung von Schlüssel/Wert-Paaren, deren Schlüssel Objekte oder nicht-registrierte Symbols sein müssen und deren Werte einen beliebigen JavaScript-Typ haben können. Sie erzeugt keine starken Referenzen auf ihre Schlüssel. Das heißt, das Vorhandensein eines Objekts als Schlüssel in einer WeakMap verhindert nicht, dass das Objekt vom Garbage Collector eingesammelt wird. Sobald ein als Schlüssel verwendetes Objekt eingesammelt wurde, werden die zugehörigen Werte in jeder WeakMap ebenfalls Kandidaten für die Garbage Collection – sofern sie nicht anderweitig stark referenziert werden. Der einzige primitive Typ, der als WeakMap-Schlüssel verwendet werden kann, ist Symbol – genauer gesagt nicht-registrierte Symbols – da nicht-registrierte Symbols garantiert eindeutig sind und nicht neu erzeugt werden können.

WeakMap erlaubt es, Daten mit Objekten zu verknüpfen, ohne zu verhindern, dass die Schlüsselobjekte eingesammelt werden – auch wenn die Werte auf die Schlüssel verweisen. Allerdings erlaubt eine WeakMap nicht, die Lebendigkeit ihrer Schlüssel zu beobachten, weshalb sie auch keine Aufzählung erlaubt; würde eine WeakMap eine Methode bereitstellen, um eine Liste ihrer Schlüssel zu erhalten, so würde diese Liste vom Zustand der Garbage Collection abhängen und Nichtdeterminismus einführen. Wenn Sie eine Liste von Schlüsseln benötigen, sollten Sie eine Map statt einer WeakMap verwenden.

Schlüsselgleichheit

Wie bei einer regulären Map basiert die Wertgleichheit auf dem SameValueZero-Algorithmus, der dem ===-Operator entspricht, da eine WeakMap nur Objekt- und Symbol-Schlüssel enthalten kann. Das bedeutet, dass die Gleichheit für Objektschlüssel auf der Objektidentität basiert. Sie werden per Referenz verglichen, nicht per Wert.

Warum WeakMap?

Eine Map-API könnte in JavaScript mit zwei Arrays (eines für Schlüssel, eines für Werte) implementiert werden, die von den vier API-Methoden gemeinsam genutzt werden. Das Setzen von Elementen würde bedeuten, gleichzeitig einen Schlüssel und einen Wert an das Ende jedes dieser Arrays zu pushen. Dadurch würden die Indizes von Schlüssel und Wert in beiden Arrays übereinstimmen. Um Werte zu erhalten, müsste man alle Schlüssel durchlaufen, um eine Übereinstimmung zu finden, und dann über den Index den entsprechenden Wert aus dem Werte-Array abrufen.

Eine solche Implementierung hätte zwei Hauptnachteile:

  1. Der erste ist ein O(n)-Setzen und -Suchen (n ist die Anzahl der Schlüssel in der Map), da beide Operationen die Liste der Schlüssel durchlaufen müssen, um einen passenden Wert zu finden.
  2. Der zweite Nachteil ist ein Memory-Leak, da die Arrays sicherstellen, dass Referenzen auf jeden Schlüssel und jeden Wert unbegrenzt gehalten werden. Diese Referenzen verhindern, dass die Schlüssel vom Garbage Collector eingesammelt werden, selbst wenn keine anderen Referenzen auf das Objekt existieren. Dies würde auch verhindern, dass die entsprechenden Werte eingesammelt werden.

Im Gegensatz dazu verweist in einer WeakMap ein Schlüsselobjekt stark auf seine Inhalte, solange der Schlüssel nicht eingesammelt wurde, danach jedoch nur noch schwach. Somit gilt für eine WeakMap:

  • Sie verhindert nicht die Garbage Collection, die schließlich Referenzen auf das Schlüsselobjekt entfernt.
  • Sie erlaubt die Garbage Collection beliebiger Werte, sofern deren Schlüsselobjekte nicht von anderer Stelle als der WeakMap referenziert werden.

Eine WeakMap kann besonders nützlich sein, wenn Schlüssel auf Informationen über den Schlüssel abgebildet werden, die nur dann wertvoll sind, wenn der Schlüssel nicht eingesammelt wurde.

Da eine WeakMap aber nicht erlaubt, die Lebendigkeit ihrer Schlüssel zu beobachten, sind ihre Schlüssel nicht aufzählbar. Es gibt keine Methode, um eine Liste der Schlüssel zu erhalten. Gäbe es sie, würde die Liste vom Zustand der Garbage Collection abhängen und Nichtdeterminismus einführen. Wenn Sie eine Liste von Schlüsseln benötigen, sollten Sie eine Map verwenden.

Parameter

Name Typ Default Beschreibung
$iterable Iterable<[object, any]>|null|undefined undefined Ein optionales iterierbares Objekt (z. B. ein Array von [schlüssel, wert]-Paaren), mit dem die WeakMap initialisiert wird. Alle Schlüssel müssen Objekte oder nicht registrierte Symbole sein.

Rückgabewert

Typ
WeakMap
Beschreibung
Eine neue WeakMap-Instanz, optional vorbelegt mit den Einträgen aus dem übergebenen Iterable.

Beispiele

WeakMap verwenden

const wm1 = new WeakMap();
const wm2 = new WeakMap();
const wm3 = new WeakMap();
const o1 = {};
const o2 = () => {};
const o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // a value can be anything, including an object or a function
wm2.set(o2, undefined);
wm2.set(wm1, wm2); // keys and values can be any objects. Even WeakMaps!

wm1.get(o2); // "azerty"
wm2.get(o2); // undefined, because that is the set value
wm2.get(o3); // undefined, because there is no key for o3 on wm2

wm1.has(o2); // true
wm2.has(o2); // true (even if the value itself is 'undefined')
wm2.has(o3); // false

wm3.set(o1, 37);
wm3.get(o1); // 37

wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false

Eine WeakMap-ähnliche Klasse mit einer .clear()-Methode implementieren

class ClearableWeakMap {
  #wm;
  constructor(init) {
    this.#wm = new WeakMap(init);
  }
  clear() {
    this.#wm = new WeakMap();
  }
  delete(k) {
    return this.#wm.delete(k);
  }
  get(k) {
    return this.#wm.get(k);
  }
  has(k) {
    return this.#wm.has(k);
  }
  set(k, v) {
    this.#wm.set(k, v);
    return this;
  }
}

Private Member emulieren

let Thing;

{
  const privateScope = new WeakMap();
  let counter = 0;

  Thing = function () {
    this.someProperty = "foo";

    privateScope.set(this, {
      hidden: ++counter,
    });
  };

  Thing.prototype.showPublic = function () {
    return this.someProperty;
  };

  Thing.prototype.showPrivate = function () {
    return privateScope.get(this).hidden;
  };
}

console.log(typeof privateScope);
// "undefined"

const thing = new Thing();

console.log(thing);
// Thing {someProperty: "foo"}

thing.showPublic();
// "foo"

thing.showPrivate();
// 1

Äquivalent mit privaten Feldern

class Thing {
  static #counter = 0;
  #hidden;
  constructor() {
    this.someProperty = "foo";
    this.#hidden = ++Thing.#counter;
  }
  showPublic() {
    return this.someProperty;
  }
  showPrivate() {
    return this.#hidden;
  }
}

const thing = new Thing();

console.log(thing);
// Thing {someProperty: "foo"}

thing.showPublic();
// "foo"

thing.showPrivate();
// 1

Metadaten zuordnen (klassischer Ansatz mit Property)

const buttons = document.querySelectorAll(".button");
buttons.forEach((button) => {
  button.clicked = false;
  button.addEventListener("click", () => {
    button.clicked = true;
    const currentButtons = [...document.querySelectorAll(".button")];
    if (currentButtons.every((button) => button.clicked)) {
      console.log("All buttons have been clicked!");
    }
  });
});

Metadaten zuordnen mit WeakMap

const buttons = document.querySelectorAll(".button");
const clicked = new WeakMap();
buttons.forEach((button) => {
  clicked.set(button, false);
  button.addEventListener("click", () => {
    clicked.set(button, true);
    const currentButtons = [...document.querySelectorAll(".button")];
    if (currentButtons.every((button) => clicked.get(button))) {
      console.log("All buttons have been clicked!");
    }
  });
});

Caching

const cache = new WeakMap();
function handleObjectValues(obj) {
  if (cache.has(obj)) {
    return cache.get(obj);
  }
  const result = Object.values(obj).map(heavyComputation);
  cache.set(obj, result);
  return result;
}

// Wichtig · Fallstricke

Keine Iteration: WeakMap bietet weder keys(), values(), entries() noch forEach() – und auch keine size-Eigenschaft. Ist eine Iteration über alle Einträge erforderlich, sollte stattdessen eine reguläre Map verwendet werden.

Schlüsseltypen: Nur Objekte und nicht registrierte Symbole (erstellt mit Symbol(), nicht mit Symbol.for()) sind als Schlüssel zulässig. Der Versuch, einen primitiven Wert als Schlüssel zu verwenden, wirft einen TypeError.

Nicht deterministischer Garbage Collection: Der Zeitpunkt, zu dem ein Eintrag nach dem Wegfall aller anderen Referenzen auf den Schlüssel tatsächlich entfernt wird, ist nicht garantiert und engine-abhängig. Man sollte sich nicht auf eine sofortige Bereinigung verlassen.

Browser-Kompatibilität: WeakMap wird seit ES2015 von allen modernen Browsern und Node.js unterstützt. In sehr alten Umgebungen (IE10 und älter) ist kein natives WeakMap verfügbar; Polyfills können die schwachen Referenzen jedoch nicht wirklich nachbilden.

Siehe auch