Start · Sprachen · JavaScript · Referenz · Proxy

Proxy

Klasse

Das <code>Proxy</code>-Objekt ermöglicht es, einen Stellvertreter für ein anderes Objekt zu erstellen, der grundlegende Operationen abfangen und neu definieren kann.

seit JavaScript ES2015 Kategorie: core

Signatur

new Proxy(target, handler)

Beschreibung

Das Proxy-Objekt erlaubt es, ein Objekt zu erstellen, das anstelle des ursprünglichen Objekts verwendet werden kann, das aber grundlegende Object-Operationen wie das Lesen, Setzen und Definieren von Eigenschaften neu definieren kann. Proxy-Objekte werden häufig verwendet, um Eigenschaftszugriffe zu protokollieren, Eingaben zu validieren, zu formatieren oder zu bereinigen und Ähnliches.

Ein Proxy wird mit zwei Parametern erstellt:

  • target: das ursprüngliche Objekt, für das ein Proxy erstellt werden soll
  • handler: ein Objekt, das definiert, welche Operationen abgefangen werden und wie die abgefangenen Operationen neu definiert werden.

Zum Beispiel erstellt dieser Code einen Proxy für das target-Objekt.

Da der Handler leer ist, verhält sich dieser Proxy genauso wie das ursprüngliche Target.

Um den Proxy anzupassen, definieren wir Funktionen auf dem Handler-Objekt.

Hier haben wir eine Implementierung des get()-Handlers bereitgestellt, der Zugriffsversuche auf Eigenschaften im Target abfängt.

Handler-Funktionen werden manchmal Traps genannt, vermutlich weil sie Aufrufe an das Target-Objekt „einfangen" (engl. trap). Die Trap in handler2 oben definiert alle Eigenschaftszugriffe neu.

Proxies werden häufig zusammen mit dem Reflect-Objekt verwendet, das einige Methoden mit denselben Namen wie die Proxy-Traps bereitstellt. Die Reflect-Methoden liefern die reflektive Semantik zum Aufruf der entsprechenden internen Methoden des Objekts. Beispielsweise können wir Reflect.get aufrufen, wenn wir das Verhalten des Objekts nicht neu definieren möchten.

Die Reflect-Methode interagiert weiterhin mit dem Objekt über die internen Methoden – sie „ent-proxyfiziert" den Proxy nicht, wenn sie auf einem Proxy aufgerufen wird. Wenn Sie Reflect-Methoden innerhalb einer Proxy-Trap verwenden und der Reflect-Methodenaufruf erneut von der Trap abgefangen wird, kann es zu einer unendlichen Rekursion kommen.

Terminologie

Die folgenden Begriffe werden verwendet, wenn über die Funktionalität von Proxies gesprochen wird.

  • handler: Das Objekt, das als zweites Argument an den Proxy-Konstruktor übergeben wird. Es enthält die Traps, die das Verhalten des Proxies definieren.
  • trap: Die Funktion, die das Verhalten für die entsprechende interne Methode des Objekts definiert. (Dies ist analog zum Konzept der Traps in Betriebssystemen.)
  • target: Objekt, das der Proxy virtualisiert. Es wird oft als Speicherbackend für den Proxy verwendet. Invarianten (Semantiken, die unverändert bleiben) bezüglich der Nichterweiterbarkeit oder nicht konfigurierbarer Eigenschaften eines Objekts werden gegen das Target geprüft.
  • invariants: Semantiken, die bei der Implementierung benutzerdefinierter Operationen unverändert bleiben. Wenn Ihre Trap-Implementierung die Invarianten eines Handlers verletzt, wird ein TypeError geworfen.

Interne Methoden von Objekten

Objekte sind Sammlungen von Eigenschaften. Die Sprache stellt jedoch keine Mechanismen bereit, um die im Objekt gespeicherten Daten direkt zu manipulieren – stattdessen definiert das Objekt einige interne Methoden, die festlegen, wie mit ihm interagiert werden kann. Wenn Sie zum Beispiel obj.x lesen, können Sie Folgendes erwarten:

  • Die Eigenschaft x wird die prototype chain hinauf gesucht, bis sie gefunden wird.
  • Wenn x eine Data Property ist, wird das value-Attribut des Property Descriptors zurückgegeben.
  • Wenn x eine Accessor Property ist, wird der Getter aufgerufen und der Rückgabewert des Getters zurückgegeben.

An diesem Prozess ist in der Sprache nichts Besonderes – es liegt einfach daran, dass gewöhnliche Objekte standardmäßig eine interne Methode [[Get]] haben, die mit diesem Verhalten definiert ist. Die Syntax des Eigenschaftszugriffs obj.x ruft einfach die [[Get]]-Methode auf dem Objekt auf, und das Objekt verwendet seine eigene Implementierung der internen Methode, um zu bestimmen, was zurückgegeben wird.

Als weiteres Beispiel unterscheiden sich Arrays von normalen Objekten, weil sie eine magische length-Eigenschaft besitzen, die beim Ändern automatisch leere Slots zuweist oder Elemente aus dem Array entfernt. Ebenso ändert das Hinzufügen von Array-Elementen automatisch die length-Eigenschaft. Das liegt daran, dass Arrays eine [[DefineOwnProperty]]-interne Methode haben, die weiß, wie length zu aktualisieren ist, wenn ein Integer-Index geschrieben wird, oder wie der Array-Inhalt zu aktualisieren ist, wenn length geschrieben wird. Solche Objekte, deren interne Methoden andere Implementierungen als bei gewöhnlichen Objekten haben, werden exotic objects genannt. Proxy ermöglicht es Entwicklern, ihre eigenen exotic objects in vollem Umfang zu definieren.

Alle Objekte haben die folgenden internen Methoden:

  • [[GetPrototypeOf]] — getPrototypeOf()
  • [[SetPrototypeOf]] — setPrototypeOf()
  • [[IsExtensible]] — isExtensible()
  • [[PreventExtensions]] — preventExtensions()
  • [[GetOwnProperty]] — getOwnPropertyDescriptor()
  • [[DefineOwnProperty]] — defineProperty()
  • [[HasProperty]] — has()
  • [[Get]] — get()
  • [[Set]] — set()
  • [[Delete]] — deleteProperty()
  • [[OwnPropertyKeys]] — ownKeys()

Funktionsobjekte haben zusätzlich die folgenden internen Methoden:

  • [[Call]] — apply()
  • [[Construct]] — construct()

Es ist wichtig zu verstehen, dass alle Interaktionen mit einem Objekt letztlich auf den Aufruf einer dieser internen Methoden hinauslaufen, und dass sie alle über Proxies anpassbar sind. Das bedeutet, dass in der Sprache fast kein Verhalten (außer bestimmten kritischen Invarianten) garantiert ist – alles wird durch das Objekt selbst definiert. Wenn Sie delete obj.x ausführen, gibt es keine Garantie dafür, dass "x" in obj danach false zurückgibt – es hängt von den Implementierungen des Objekts für [[Delete]] und [[HasProperty]] ab. Ein delete obj.x könnte Dinge in die Konsole schreiben, globalen Zustand ändern oder sogar eine neue Eigenschaft definieren, anstatt die bestehende zu löschen, obwohl solche Semantiken im eigenen Code vermieden werden sollten.

Alle internen Methoden werden von der Sprache selbst aufgerufen und sind in JavaScript-Code nicht direkt zugänglich. Der Reflect-Namensraum bietet Methoden, die kaum mehr tun, als die internen Methoden aufzurufen, abgesehen von etwas Eingabenormalisierung/-validierung. Auf der Seite jeder Trap listen wir mehrere typische Situationen auf, in denen die Trap aufgerufen wird, aber diese internen Methoden werden an vielen Stellen aufgerufen. Zum Beispiel lesen und schreiben Array-Methoden über diese internen Methoden auf das Array zu, sodass Methoden wie push() ebenfalls die Traps get() und set() auslösen würden.

Die meisten internen Methoden sind in ihrer Funktion selbsterklärend. Die einzigen zwei, die verwechselt werden könnten, sind [[Set]] und [[DefineOwnProperty]]. Für normale Objekte ruft ersteres Setter auf; letzteres nicht. (Und [[Set]] ruft intern [[DefineOwnProperty]] auf, wenn keine bestehende Eigenschaft vorhanden ist oder die Eigenschaft eine Data Property ist.) Während Sie wissen mögen, dass die Syntax obj.x = 1 [[Set]] verwendet und Object.defineProperty() [[DefineOwnProperty]] verwendet, ist nicht sofort ersichtlich, welche Semantiken andere eingebaute Methoden und Syntaxen verwenden. Zum Beispiel verwenden class fields die [[DefineOwnProperty]]-Semantik, weshalb Setter, die in der Superklasse definiert sind, nicht aufgerufen werden, wenn ein Feld in der abgeleiteten Klasse deklariert wird.

Konstruktor

  • Proxy(): Erstellt ein neues Proxy-Objekt.

Hinweis: Es gibt keine Proxy.prototype-Eigenschaft, daher haben Proxy-Instanzen keine besonderen Eigenschaften oder Methoden.

Statische Methoden

  • Proxy.revocable(): Erstellt ein widerrufbares Proxy-Objekt.

Parameter

Name Typ Default Beschreibung
$target Pflicht Object — Das ursprüngliche Objekt, das durch den Proxy virtualisiert wird.
$handler Pflicht Object — Ein Objekt, dessen Eigenschaften Funktionen sind, die das Verhalten des Proxies definieren, wenn eine Operation auf ihm ausgeführt wird (Traps).

Rückgabewert

Typ
Proxy
Beschreibung
Ein neues Proxy-Objekt, das target mit den definierten handler-Traps umhüllt.

Beispiele

Grundlegendes Beispiel

const handler = {
  get(obj, prop) {
    return prop in obj ? obj[prop] : 37;
  },
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b); // 1, undefined

console.log("c" in p, p.c); // false, 37

No-op-Forwarding-Proxy

const target = {};
const p = new Proxy(target, {});

p.a = 37; // Operation forwarded to the target

console.log(target.a); // 37 (The operation has been properly forwarded!)

Keine Weiterleitung privater Felder

class Secret {
  #secret;
  constructor(secret) {
    this.#secret = secret;
  }
  get secret() {
    return this.#secret.replace(/\d+/, "[REDACTED]");
  }
}

const secret = new Secret("123456");
console.log(secret.secret); // [REDACTED]
// Looks like a no-op forwarding...
const proxy = new Proxy(secret, {});
console.log(proxy.secret); // TypeError: Cannot read private member #secret from an object whose class did not declare it

Behebung durch Umleitung von this

const proxy = new Proxy(secret, {
  get(target, prop, receiver) {
    // By default, it looks like Reflect.get(target, prop, receiver)
    // which has a different value of `this`
    return target[prop];
  },
});
console.log(proxy.secret);

Methoden mit umgeleitetem this

class Secret {
  #x = 1;
  x() {
    return this.#x;
  }
}

const secret = new Secret();
const proxy = new Proxy(secret, {
  get(target, prop, receiver) {
    const value = target[prop];
    if (value instanceof Function) {
      return function (...args) {
        return value.apply(this === receiver ? target : this, args);
      };
    }
    return value;
  },
});
console.log(proxy.x());

Map-Objekt-Proxy

const proxy = new Proxy(new Map(), {});
console.log(proxy.size); // TypeError: get size method called on incompatible Proxy

Validierung

const validator = {
  set(obj, prop, value) {
    if (prop === "age") {
      if (!Number.isInteger(value)) {
        throw new TypeError("The age is not an integer");
      }
      if (value > 200) {
        throw new RangeError("The age seems invalid");
      }
    }

    // The default behavior to store the value
    obj[prop] = value;

    // Indicate success
    return true;
  },
};

const person = new Proxy({}, validator);

person.age = 100;
console.log(person.age); // 100
person.age = "young"; // Throws an exception
person.age = 300; // Throws an exception

Manipulation von DOM-Knoten

const view = new Proxy(
  {
    selected: null,
  },
  {
    set(obj, prop, newVal) {
      const oldVal = obj[prop];

      if (prop === "selected") {
        if (oldVal) {
          oldVal.setAttribute("aria-selected", "false");
        }
        if (newVal) {
          newVal.setAttribute("aria-selected", "true");
        }
      }

      // The default behavior to store the value
      obj[prop] = newVal;

      // Indicate success
      return true;
    },
  },
);

const item1 = document.getElementById("item-1");
const item2 = document.getElementById("item-2");

// select item1:
view.selected = item1;

console.log(`item1: ${item1.getAttribute("aria-selected")}`);
// item1: true

// selecting item2 de-selects item1:
view.selected = item2;

console.log(`item1: ${item1.getAttribute("aria-selected")}`);
// item1: false

console.log(`item2: ${item2.getAttribute("aria-selected")}`);
// item2: true

Wertekorrektur und eine zusätzliche Eigenschaft

const products = new Proxy(
  {
    browsers: ["Firefox", "Chrome"],
  },
  {
    get(obj, prop) {
      // An extra property
      if (prop === "latestBrowser") {
        return obj.browsers[obj.browsers.length - 1];
      }

      // The default behavior to return the value
      return obj[prop];
    },
    set(obj, prop, value) {
      // An extra property
      if (prop === "latestBrowser") {
        obj.browsers.push(value);
        return true;
      }

      // Convert the value if it is not an array
      if (typeof value === "string") {
        value = [value];
      }

      // The default behavior to store the value
      obj[prop] = value;

      // Indicate success
      return true;
    },
  },
);

console.log(products.browsers);
//  ['Firefox', 'Chrome']

products.browsers = "Safari";
//  pass a string (by mistake)

console.log(products.browsers);
//  ['Safari'] <- no problem, the value is an array

products.latestBrowser = "Edge";

console.log(products.browsers);
//  ['Safari', 'Edge']

console.log(products.latestBrowser);
//  'Edge'

// Wichtig · Fallstricke

Es gibt keine Proxy.prototype-Eigenschaft, daher haben Proxy-Instanzen keine besonderen Eigenschaften oder Methoden. Beachten Sie, dass ein „No-op"-Forwarding-Proxy zwar für einfache JavaScript-Objekte funktioniert, jedoch nicht für native Objekte wie DOM-Elemente, Map-Objekte oder alles, was interne Slots hat.