Skip to content

Türchen 11

Published: at 07:00 AMSuggest Changes

Proxy – Kontrolle über Objekte übernehmen

Seit der Einführung von ES6 hat JavaScript viele neue Features erhalten, die Entwicklern mehr Kontrolle und Flexibilität bieten. Eines dieser mächtigen Werkzeuge ist der Proxy. Mit Proxies kannst du das Verhalten von Objekten abfangen und anpassen, indem du auf grundlegende Operationen wie das Lesen, Schreiben oder Löschen von Eigenschaften Einfluss nimmst.

Was ist ein Proxy?

Ein Proxy ist ein Wrapper-Objekt, das ein Zielobjekt repräsentiert und es dir ermöglicht, grundlegende Objektoperationen zu überwachen und zu modifizieren. Dies geschieht über sogenannte Handler, die Funktionen bereitstellen, um verschiedene Aktionen abzufangen, wie z. B. das Zugreifen auf Eigenschaften oder das Aufrufen von Methoden.

Grundlegende Syntax

Die Erstellung eines Proxys erfolgt mit dem Proxy-Konstruktor, der zwei Argumente akzeptiert:

  1. Zielobjekt (target): Das Originalobjekt, das der Proxy umhüllt.
  2. Handler: Ein Objekt mit “Traps” (Fallen), die die Operationen abfangen.

Beispiel:

const targetObject = {};

const handler = {
  get: function(target, property, receiver) {
    console.log(`Eigenschaft ${property} wurde gelesen.`);
    return target[property];
  }
};

const proxy = new Proxy(targetObject, handler);

proxy.name = 'Max';
console.log(proxy.name);
// Ausgabe:
// Eigenschaft name wurde gelesen.
// Max

Was passiert in diesem Beispiel:

In diesem Codebeispiel wird ein leeres Objekt targetObject erstellt. Ein Proxy-Handler wird definiert, der die get-Operation abfängt. Der get-Handler wird aufgerufen, wenn auf eine Eigenschaft des Proxys zugegriffen wird. In diesem Fall protokolliert der Handler eine Nachricht, die besagt, dass die Eigenschaft gelesen wurde, und gibt dann den entsprechenden Wert aus dem Zielobjekt target zurück.

Ein Proxy-Objekt proxy wird erstellt, das das ursprüngliche targetObject umhüllt und den definierten Handler verwendet. Wenn die Eigenschaft name auf dem Proxy-Objekt gesetzt wird (proxy.name = ‘Max’), wird dieser Wert im targetObject gespeichert.

Beim anschließenden Zugriff auf proxy.name wird der get-Handler aufgerufen, der die Nachricht Eigenschaft name wurde gelesen. protokolliert und den Wert Max zurückgibt.

Häufig verwendete Traps

Hier sind einige der am häufigsten verwendeten Traps:

Beispiele für die Verwendung

1. Validierung von Eigenschaftswerten

Mit der set-Trap kannst du Eingabewerte validieren, bevor sie dem Objekt zugewiesen werden.

const user = {};

const handler = {
  set(target, property, value) {
    if (property === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('Das Alter muss eine ganze Zahl sein.');
      }
      if (value < 0) {
        throw new RangeError('Das Alter kann nicht negativ sein.');
      }
    }
    target[property] = value;
    return true;
  }
};

const proxyUser = new Proxy(user, handler);

proxyUser.age = 30; // OK
console.log(proxyUser.age); // 30

proxyUser.age = -5; // RangeError: Das Alter kann nicht negativ sein.

Was hier passiert:
In diesem Codebeispiel wird ein leeres Objekt user erstellt. Ein Proxy-Handler wird definiert, der die set-Operation abfängt. Dieser Handler überprüft, ob die Eigenschaft, die gesetzt werden soll, age (Alter) ist. Wenn dies der Fall ist, stellt der Handler sicher, dass der Wert eine ganze Zahl ist. Falls nicht, wird ein TypeError ausgelöst. Zusätzlich wird überprüft, ob der Wert negativ ist. Falls ja, wird ein RangeError ausgelöst.

Wenn die Überprüfungen erfolgreich sind, wird die Eigenschaft auf dem Zielobjekt target gesetzt und true zurückgegeben, um anzuzeigen, dass die Operation erfolgreich war.

Ein Proxy-Objekt proxyUser wird erstellt, das das ursprüngliche user-Objekt umhüllt und den definierten Handler verwendet. Wenn proxyUser.age auf 30 gesetzt wird, erfolgt dies ohne Probleme, und der Wert 30 wird korrekt ausgegeben. Wenn jedoch versucht wird, proxyUser.age auf -5 zu setzen, wird ein RangeError ausgelöst, da das Alter nicht negativ sein darf.

2. Eigenschaftszugriffe protokollieren

Mit der get-Trap kannst du alle Lesezugriffe auf die Eigenschaften eines Objekts protokollieren.

const product = {
  name: 'Laptop',
  price: 1500
};

const handler = {
  get(target, property) {
    console.log(`Zugriff auf Eigenschaft "${property}": ${target[property]}`);
    return target[property];
  }
};

const proxyProduct = new Proxy(product, handler);

console.log(proxyProduct.name);
// Ausgabe:
// Zugriff auf Eigenschaft "name": Laptop
// Laptop

console.log(proxyProduct.price);
// Ausgabe:
// Zugriff auf Eigenschaft "price": 1500
// 1500

Was hier passiert:
In diesem Codebeispiel wird ein Objekt product erstellt, das zwei Eigenschaften hat: name und price. Ein Proxy-Handler wird definiert, der die get-Operation abfängt. Dieser Handler protokolliert jeden Zugriff auf die Eigenschaften des Objekts und gibt den entsprechenden Wert zurück.

Ein Proxy-Objekt proxyProduct wird erstellt, das das ursprüngliche product-Objekt umhüllt und den definierten Handler verwendet. Wenn auf proxyProduct.name zugegriffen wird, protokolliert der Handler den Zugriff und gibt den Wert Laptop zurück. Ebenso wird beim Zugriff auf proxyProduct.price der Zugriff protokolliert und der Wert 1500 zurückgegeben.

Die Konsolenausgabe zeigt, dass der Zugriff auf die Eigenschaften name und price protokolliert wird, bevor die tatsächlichen Werte ausgegeben werden. Dies verdeutlicht, wie Proxies verwendet werden können, um zusätzliche Logik beim Zugriff auf Objekteigenschaften hinzuzufügen.

3. Eigenschaften schreibgeschützt machen

Mit der set-Trap kannst du verhindern, dass bestimmte Eigenschaften geändert werden.

const settings = {
  theme: 'dark'
};

const handler = {
  set(target, property, value) {
    if (property === 'theme') {
      console.log('Die Eigenschaft "theme" ist schreibgeschützt.');
      return false;
    }
    target[property] = value;
    return true;
  }
};

const proxySettings = new Proxy(settings, handler);

proxySettings.theme = 'light'; // Versuch, den Wert zu ändern
// Ausgabe: Die Eigenschaft "theme" ist schreibgeschützt.

console.log(proxySettings.theme); // 'dark'

Was hier passiert:
In diesem Codebeispiel wird ein Objekt settings erstellt, das eine Eigenschaft theme mit dem Wert dark enthält. Ein Proxy-Handler wird definiert, der die set-Operation abfängt. Dieser Handler überprüft, ob die Eigenschaft, die gesetzt werden soll, theme ist. Wenn dies der Fall ist, wird eine Nachricht protokolliert, die besagt, dass die Eigenschaft theme schreibgeschützt ist, und die Operation wird mit false abgebrochen.

Wenn die Eigenschaft nicht theme ist, wird der neue Wert auf dem Zielobjekt target gesetzt und true zurückgegeben, um anzuzeigen, dass die Operation erfolgreich war.

Ein Proxy-Objekt proxySettings wird erstellt, das das ursprüngliche settings-Objekt umhüllt und den definierten Handler verwendet. Wenn versucht wird, proxySettings.theme auf light zu setzen, wird dies vom Handler abgefangen und die Nachricht “Die Eigenschaft ‘theme’ ist schreibgeschützt.” wird ausgegeben. Der Wert der Eigenschaft theme bleibt unverändert dark.

Die Konsolenausgabe zeigt, dass der Versuch, die Eigenschaft theme zu ändern, fehlschlägt und der ursprüngliche Wert dark beibehalten wird. Dies verdeutlicht, wie Proxies verwendet werden können, um bestimmte Eigenschaften eines Objekts schreibgeschützt zu machen.

4. Standardwerte für nicht vorhandene Eigenschaften

Mit der get-Trap kannst du Standardwerte zurückgeben, wenn eine Eigenschaft nicht existiert.

const dictionary = {};

const handler = {
  get(target, property) {
    return property in target ? target[property] : `Übersetzung für "${property}" nicht gefunden.`;
  }
};

const proxyDictionary = new Proxy(dictionary, handler);

proxyDictionary.hallo = 'hello';

console.log(proxyDictionary.hallo); // 'hello'
console.log(proxyDictionary.welt);  // 'Übersetzung für "welt" nicht gefunden.'

Was hier passiert:
In diesem Codebeispiel wird ein leeres Objekt dictionary erstellt. Ein Proxy-Handler wird definiert, der die get-Operation abfängt. Dieser Handler überprüft, ob die angeforderte Eigenschaft im Zielobjekt target vorhanden ist. Wenn die Eigenschaft existiert, wird ihr Wert zurückgegeben. Andernfalls wird eine Nachricht zurückgegeben, die besagt, dass die Übersetzung für die angeforderte Eigenschaft nicht gefunden wurde.

Ein Proxy-Objekt proxyDictionary wird erstellt, das das ursprüngliche dictionary-Objekt umhüllt und den definierten Handler verwendet. Wenn proxyDictionary.hallo auf hello gesetzt wird, wird dieser Wert im dictionary-Objekt gespeichert. Beim Zugriff auf proxyDictionary.hallo gibt der Handler den Wert hello zurück, da die Eigenschaft im Zielobjekt vorhanden ist.

Beim Zugriff auf proxyDictionary.welt hingegen, stellt der Handler fest, dass die Eigenschaft welt nicht im Zielobjekt vorhanden ist und gibt die Nachricht Übersetzung für “welt” nicht gefunden. zurück.

Dieses Beispiel zeigt, wie Proxies verwendet werden können, um Standardwerte oder Fehlermeldungen zurückzugeben, wenn auf nicht vorhandene Eigenschaften eines Objekts zugegriffen wird.

Proxy auf Funktionen anwenden

Proxies können auch auf Funktionen angewendet werden, um deren Aufrufverhalten zu verändern.

function sum(a, b) {
  return a + b;
}

const handler = {
  apply(target, thisArg, argumentsList) {
    console.log(`sum wurde mit den Argumenten ${argumentsList} aufgerufen.`);
    return target(...argumentsList);
  }
};

const proxySum = new Proxy(sum, handler);

console.log(proxySum(5, 10));
// Ausgabe:
// sum wurde mit den Argumenten 5,10 aufgerufen.
// 15

Was hier passiert:
In diesem Codebeispiel wird eine einfache Funktion sum definiert, die zwei Argumente a und b entgegennimmt und deren Summe zurückgibt. Ein Proxy-Handler wird definiert, der die apply-Operation abfängt. Diese Operation wird aufgerufen, wenn die Proxy-Funktion ausgeführt wird.

Der apply-Handler protokolliert die Argumente, mit denen die Funktion aufgerufen wurde, und gibt dann das Ergebnis der ursprünglichen sum-Funktion zurück, indem er die Argumente mit dem Spread-Operator … weiterleitet.

Ein Proxy-Objekt proxySum wird erstellt, das die ursprüngliche sum-Funktion umhüllt und den definierten Handler verwendet. Wenn proxySum(5, 10) aufgerufen wird, protokolliert der Handler die Nachricht sum wurde mit den Argumenten 5,10 aufgerufen. und gibt das Ergebnis 15 zurück.

Dieses Beispiel zeigt, wie Proxies verwendet werden können, um zusätzliche Logik hinzuzufügen, wenn Funktionen aufgerufen werden, ohne die ursprüngliche Funktion zu verändern.

Einschränkungen und Vorsichtsmaßnahmen

  1. Unveränderliche native Objekte:
    Manche native Objekte, wie etwa ein Objekt, das versiegelt wurde, lassen sich nicht ohne weiteres durch einen Proxy in ihrem Verhalten ändern. Versucht man etwa, ein versiegeltes Objekt zu modifizieren, erhält man einen Fehler.

    const sealedObj = Object.seal({ name: 'Max' });
    
    const handler = {
      set(target, property, value) {
        target[property] = value;
        return true;
      }
    };
    
    const proxy = new Proxy(sealedObj, handler);
    
    try {
      proxy.age = 30; // Versuch, eine neue Eigenschaft anzulegen
    } catch (error) {
      console.error('Fehler beim Setzen einer neuen Eigenschaft auf ein versiegeltes Objekt:', error);
    }
    // Ausgabe: Fehler (TypeError), da das Objekt versiegelt ist und keine neuen Props hinzugefügt werden dürfen.
    

    Was hier passiert:
    In diesem Codebeispiel wird ein Objekt sealedObj erstellt und mit Object.seal versiegelt. Ein versiegeltes Objekt erlaubt keine Hinzufügung oder Entfernung von Eigenschaften, aber die vorhandenen Eigenschaften können geändert werden.

    Ein Proxy-Handler wird definiert, der die set-Operation abfängt. Dieser Handler versucht, den Wert der angegebenen Eigenschaft auf dem Zielobjekt target zu setzen und gibt true zurück, um anzuzeigen, dass die Operation erfolgreich war.

    Ein Proxy-Objekt proxy wird erstellt, das das versiegelte sealedObj umhüllt und den definierten Handler verwendet. Wenn versucht wird, proxy.age auf 30 zu setzen, wird dies vom Proxy-Handler abgefangen. Da das Zielobjekt jedoch versiegelt ist, löst dieser Versuch einen TypeError aus, weil keine neuen Eigenschaften zu einem versiegelten Objekt hinzugefügt werden dürfen.

    Der Fehler wird im catch-Block abgefangen und eine entsprechende Fehlermeldung wird ausgegeben: “Fehler beim Setzen einer neuen Eigenschaft auf ein versiegeltes Objekt”. Dieses Beispiel zeigt, wie Proxies mit versiegelten Objekten interagieren und welche Einschränkungen dabei bestehen.

  2. Proxies auf DOM-Elementen:

    Manche Browser-APIs oder DOM-Objekte sind intern so implementiert, dass das Proxen nicht wie erwartet funktioniert. Nicht alle Eigenschaften und Methoden lassen sich einfangen oder umleiten.

    // Theoretisches Beispiel, kann je nach Browser nicht wie erwartet funktionieren
    const div = document.createElement('div');
    
    const handler = {
      get(target, property) {
        console.log(`Eigenschaft "${property}" wurde gelesen.`);
        return target[property];
      }
    };
    
    const divProxy = new Proxy(div, handler);
    
    // Zugriff auf native DOM-Methoden kann zu unerwarteten Effekten führen
    console.log(divProxy.querySelector('span')); // Eventuell kein oder unerwartetes Logging
    

    Was hier passiert:
    In diesem Codebeispiel wird ein div-Element mit document.createElement(‘div’) erstellt. Ein Proxy-Handler wird definiert, der die get-Operation abfängt. Dieser Handler protokolliert jeden Zugriff auf die Eigenschaften des div-Elements und gibt den entsprechenden Wert zurück.

    Ein Proxy-Objekt divProxy wird erstellt, das das ursprüngliche div-Element umhüllt und den definierten Handler verwendet. Wenn auf eine Eigenschaft des divProxy zugegriffen wird, wie z.B. querySelector, protokolliert der Handler den Zugriff und gibt die Eigenschaft des ursprünglichen div-Elements zurück.

    Der Kommentar im Code weist darauf hin, dass der Zugriff auf native DOM-Methoden wie querySelector zu unerwarteten Effekten führen kann. Dies liegt daran, dass der Proxy möglicherweise nicht alle internen Mechanismen und Besonderheiten der DOM-Methoden korrekt handhabt. In diesem Fall wird divProxy.querySelector(‘span’) aufgerufen, was möglicherweise kein oder unerwartetes Logging zur Folge hat.

    Dieses Beispiel zeigt, wie Proxies verwendet werden können, um den Zugriff auf DOM-Elemente zu überwachen, und weist auf potenzielle Probleme hin, die bei der Verwendung von Proxies mit nativen DOM-Methoden auftreten können.

  3. Performance-Überlegungen:

    Proxies fügen eine Abstraktionsschicht hinzu. Bei sehr hohem Datenaufkommen oder zahlreichen Zugriffen auf Eigenschaften kann das spürbar auf die Performance gehen.

    const largeObject = {};
    for (let i = 0; i < 100000; i++) {
      largeObject[`key${i}`] = i;
    }
    
    const handler = {
      get(target, property) {
        // Jeder Zugriff wird geloggt – bei vielen Zugriffen Performance beachten!
        return target[property];
      }
    };
    
    const proxiedLargeObject = new Proxy(largeObject, handler);
    
    // Viele Zugriffe, jeder wird verlangsamt, da ein get-Handler eingreift.
    for (let i = 0; i < 100000; i++) {
      const val = proxiedLargeObject[`key${i}`];
    }
    

    Was hier passiert:
    In diesem Codebeispiel wird ein großes Objekt largeObject erstellt und mit 100.000 Schlüssel-Wert-Paaren gefüllt. Jeder Schlüssel hat das Format key0, key1, key2 usw., und die entsprechenden Werte sind die Zahlen von 0 bis 99.999.

    Ein Proxy-Handler wird definiert, der die get-Operation abfängt. Dieser Handler protokolliert jeden Zugriff auf die Eigenschaften des Objekts und gibt den entsprechenden Wert zurück. Der Kommentar im Handler weist darauf hin, dass bei vielen Zugriffen die Performance beeinträchtigt werden kann, da jeder Zugriff geloggt wird.

    Ein Proxy-Objekt proxiedLargeObject wird erstellt, das das ursprüngliche largeObject umhüllt und den definierten Handler verwendet. In der anschließenden Schleife wird 100.000 Mal auf die Eigenschaften des proxiedLargeObject zugegriffen. Jeder dieser Zugriffe wird vom get-Handler abgefangen, was die Performance verlangsamen kann.

    Dieses Beispiel zeigt, wie Proxies verwendet werden können, um den Zugriff auf Objekteigenschaften zu überwachen, und weist auf die potenziellen Performance-Probleme hin, die bei einer großen Anzahl von Zugriffen auftreten können.

  4. Inkompatibilität mit einigen Sprachfeatures:
    Nicht alle Sprachfeatures spielen gut mit Proxies zusammen. Zum Beispiel funktionieren einige intern genutzte Methoden, die auf den internen Slots von Objekten basieren, nicht wie erwartet mit Proxy.

    Untenstehend ist ein Beispiel, bei dem die Inkompatibilität von Proxys mit bestimmten Sprachfeatures sichtbar wird. Insbesondere private Felder in Klassen können nicht über einen Proxy abgefangen oder manipuliert werden, da sie nicht über die gewöhnlichen Property-Operationen zugänglich sind, sondern einen eigenen, lexikalisch geschützten Namensraum besitzen.

    class MyClass {
      #privateValue = 42;  // Privates Feld
      
      getValue() {
        return this.#privateValue;
      }
    }
    
    const instance = new MyClass();
    
    const handler = {
      get(target, property, receiver) {
        console.log(`Zugriff auf Eigenschaft "${property}"`);
        return Reflect.get(target, property, receiver);
      }
    };
    
    const proxyInstance = new Proxy(instance, handler);
    
    console.log(proxyInstance.getValue()); 
    // Ausgabe:
    // Zugriff auf Eigenschaft "getValue"
    // 42
    
    // Versuch, direkt auf das private Feld zuzugreifen (funktioniert nicht):
    try {
      // Dies ist ungültige Syntax, da private Felder nicht über den Proxy oder allgemein außerhalb der Klasse zugreifbar sind.
      console.log(proxyInstance.#privateValue);
    } catch (error) {
      console.error('Zugriff auf private Fields ist nicht möglich, selbst mit Proxy:', error);
    }
    

    Was hier passiert:
    In diesem Codebeispiel wird eine Klasse MyClass definiert, die ein privates Feld #privateValue enthält. Private Felder in JavaScript werden durch das Präfix # gekennzeichnet und sind nur innerhalb der Klasse zugänglich. Die Methode getValue gibt den Wert dieses privaten Feldes zurück.

    Eine Instanz dieser Klasse wird erstellt und in der Variablen instance gespeichert. Anschließend wird ein Proxy-Handler definiert, der die get-Operation abfängt. Dieser Handler protokolliert jeden Zugriff auf Eigenschaften der Instanz und verwendet dann Reflect.get, um den eigentlichen Zugriff durchzuführen.

    Ein Proxy-Objekt proxyInstance wird erstellt, das die ursprüngliche Instanz instance umhüllt und den definierten Handler verwendet. Wenn proxyInstance.getValue() aufgerufen wird, wird zuerst eine Nachricht protokolliert, die den Zugriff auf die Methode getValue anzeigt, und dann wird der Wert 42 zurückgegeben.

    Der Versuch, direkt auf das private Feld #privateValue über den Proxy zuzugreifen, schlägt fehl und löst einen Fehler aus. Dies liegt daran, dass private Felder nicht über den Proxy oder allgemein außerhalb der Klasse zugänglich sind. Der Fehler wird im catch-Block abgefangen und eine entsprechende Fehlermeldung wird ausgegeben.

    Diese Inkompatibilität zeigt, dass Proxies zwar mächtig sind, aber nicht alle internen Mechanismen von JavaScript-Objekten oder Klassenstrukturen abfangen oder manipulieren können.

Fazit

Proxies sind ein leistungsstarkes Feature in JavaScript, das es Entwicklern ermöglicht, das Verhalten von Objekten flexibel zu steuern und anzupassen. Sie eröffnen neue Möglichkeiten für Validierung, Logging, Sicherheit und vieles mehr. Obwohl sie mit Vorsicht und Verständnis eingesetzt werden sollten, können Proxies den Code erheblich verbessern und erweitern.

Experimentiere mit Proxies in deinem nächsten Projekt und entdecke, wie sie dir helfen können, die Kontrolle über deine Objekte zu übernehmen!


Previous Post
Türchen 12
Next Post
Türchen 10