Proxy
Baseline Widely available
This feature is well established and works across many devices and browser versions. It’s been available across browsers since September 2016.
Das Proxy
-Objekt ermöglicht es Ihnen, einen Proxy für ein anderes Objekt zu erstellen, der grundlegende Operationen für dieses Objekt abfangen und neu definieren kann.
Beschreibung
Das Proxy
-Objekt ermöglicht es Ihnen, ein Objekt zu erstellen, das anstelle des Originalobjekts verwendet werden kann, aber grundlegende Object
-Operationen wie das Abrufen, Setzen und Definieren von Eigenschaften neu definieren kann. Proxy-Objekte werden häufig verwendet, um Zugriffe auf Eigenschaften zu protokollieren, Eingaben zu validieren, zu formatieren oder zu bereinigen usw.
Sie erstellen einen Proxy
mit zwei Parametern:
target
: das ursprüngliche Objekt, das Sie proxieren möchtenhandler
: ein Objekt, das definiert, welche Operationen abgefangen und wie die abgefangenen Operationen neu definiert werden.
Dieses Codebeispiel erstellt beispielsweise einen Proxy für das target
-Objekt.
const target = {
message1: "hello",
message2: "everyone",
};
const handler1 = {};
const proxy1 = new Proxy(target, handler1);
Da der Handler leer ist, verhält sich dieser Proxy genauso wie das ursprüngliche Ziel:
console.log(proxy1.message1); // hello
console.log(proxy1.message2); // everyone
Um den Proxy anzupassen, definieren wir Funktionen im Handler-Objekt:
const target = {
message1: "hello",
message2: "everyone",
};
const handler2 = {
get(target, prop, receiver) {
return "world";
},
};
const proxy2 = new Proxy(target, handler2);
Hier haben wir eine Implementierung des get()
-Handlers bereitgestellt, der Zugriffsversuche auf Eigenschaften des Ziels abfängt.
Handler-Funktionen werden manchmal Traps genannt, vermutlich weil sie Aufrufe des Zielobjekts abfangen. Der Trap in handler2
oben definiert alle Eigenschaftszugriffe neu:
console.log(proxy2.message1); // world
console.log(proxy2.message2); // world
Proxies werden häufig mit dem Reflect
-Objekt verwendet, das einige Methoden mit denselben Namen wie die Proxy
-Traps bereitstellt. Die Reflect
-Methoden bieten die reflexiven Semantiken zum Aufrufen der entsprechenden Objekt-internen Methoden. Beispielsweise können wir Reflect.get
aufrufen, wenn wir das Verhalten des Objekts nicht neu definieren möchten:
const target = {
message1: "hello",
message2: "everyone",
};
const handler3 = {
get(target, prop, receiver) {
if (prop === "message2") {
return "world";
}
return Reflect.get(...arguments);
},
};
const proxy3 = new Proxy(target, handler3);
console.log(proxy3.message1); // hello
console.log(proxy3.message2); // world
Die Reflect
-Methode interagiert weiterhin mit dem Objekt über interne Objektmethoden – sie "entproxifiziert" den Proxy nicht, wenn sie auf einem Proxy aufgerufen wird. Wenn Sie Reflect
-Methoden innerhalb eines Proxy-Traps verwenden und der Reflect
-Methodenaufruf erneut von dem Trap abgefangen wird, kann es zu einer Endlosschleife 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 Proxys definieren. - trap
-
Die Funktion, die das Verhalten für die entsprechende Objekt-interne Methode definiert. (Dies ist analog zu dem Konzept der Traps in Betriebssystemen.)
- target
-
Objekt, das vom Proxy virtualisiert wird. Es wird häufig als Speicher-Backend für den Proxy verwendet. Invarianten (Semantiken, die unverändert bleiben) in Bezug auf die Nichterweiterbarkeit oder die nicht konfigurierbaren Eigenschaften von Objekten werden gegen das Ziel überprüft.
- invariants
-
Semantiken, die unverändert bleiben, wenn benutzerdefinierte Operationen implementiert werden. Wenn Ihre Traps-Implementierung die Invarianten eines Handlers verletzt, wird ein
TypeError
ausgelöst.
Objekt-interne Methoden
Objekte sind Sammlungen von Eigenschaften. Die Sprache bietet jedoch keine Mechanismen, um direkt auf gespeicherte Daten im Objekt zuzugreifen – vielmehr definiert das Objekt einige interne Methoden, die festlegen, wie darauf zugegriffen werden kann. Zum Beispiel, wenn Sie obj.x
lesen, könnten Sie erwarten, dass folgendes passiert:
- Die Eigenschaft
x
wird in der Prototypkette gesucht, bis sie gefunden wird. - Wenn
x
eine Dateneigenschaft ist, wird dasvalue
-Attribut des Eigenschaftenbeschreibers zurückgegeben. - Wenn
x
eine Accessor-Eigenschaft ist, wird der Getter aufgerufen, und der Rückgabewert des Getters wird zurückgegeben.
An diesem Prozess ist in der Sprache nichts besonders – es liegt einfach daran, dass gewöhnliche Objekte standardmäßig eine [[Get]]
-Interne Methode haben, die mit diesem Verhalten definiert ist. Die obj.x
-Eigenschaftszugriffssyntax ruft einfach die [[Get]]
-Methode auf dem Objekt auf, und das Objekt verwendet seine eigene interne Methodenimplementierung, um zu bestimmen, was zurückzugeben ist.
Ein weiteres Beispiel ist, dass sich Arrays von normalen Objekten unterscheiden, da sie eine magische length
-Eigenschaft haben, die beim Modifizieren automatisch leere Slots allokiert oder Elemente aus dem Array entfernt. Ebenso ändert das Hinzufügen von Array-Elementen automatisch die length
-Eigenschaft. Dies liegt daran, dass Arrays eine [[DefineOwnProperty]]
-Interne Methode haben, die weiß, dass length
aktualisiert werden muss, wenn ein ganzzahliger Index geschrieben wird, oder die Array-Inhalte aktualisiert werden müssen, wenn length
geschrieben wird. Solche Objekte, deren interne Methoden unterschiedliche Implementierungen von gewöhnlichen Objekten haben, werden als exotische Objekte bezeichnet. Mit Proxy
können Entwickler ihre eigenen exotischen Objekte mit voller Kapazität definieren.
Alle Objekte haben die folgenden internen Methoden:
Innere Methode | Entsprechender Trap |
---|---|
[[GetPrototypeOf]] |
getPrototypeOf() |
[[SetPrototypeOf]] |
setPrototypeOf() |
[[IsExtensible]] |
isExtensible() |
[[PreventExtensions]] |
preventExtensions() |
[[GetOwnProperty]] |
getOwnPropertyDescriptor() |
[[DefineOwnProperty]] |
defineProperty() |
[[HasProperty]] |
has() |
[[Get]] |
get() |
[[Set]] |
set() |
[[Delete]] |
deleteProperty() |
[[OwnPropertyKeys]] |
ownKeys() |
Funktionsobjekte haben auch die folgenden internen Methoden:
Innere Methode | Entsprechender Trap |
---|---|
[[Call]] |
apply() |
[[Construct]] |
construct() |
Es ist wichtig zu erkennen, dass alle Interaktionen mit einem Objekt letztendlich auf den Aufruf einer dieser internen Methoden hinauslaufen und dass sie alle durch Proxies anpassbar sind. Das bedeutet, dass fast kein Verhalten (außer bestimmten kritischen Invarianten) in der Sprache garantiert ist – alles wird vom Objekt selbst definiert. Wenn Sie delete obj.x
ausführen, gibt es keine Garantie, dass "x" in obj
anschließend false
zurückgibt – es hängt von den Implementierungen des Objekts für [[Delete]]
und [[HasProperty]]
ab. Ein delete obj.x
kann Dinge in die Konsole protokollieren, einen globalen Zustand ändern oder sogar eine neue Eigenschaft definieren, anstatt die vorhandene zu löschen, obwohl Sie solche Semantiken in Ihrem eigenen Code vermeiden 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 nur wenig mehr als den Aufruf der internen Methoden ausführen, abgesehen von einigen Eingabenormalisierungen/-validierungen. Auf jeder Trap-Seite listen wir mehrere typische Situationen auf, wann der Trap aufgerufen wird, aber diese internen Methoden werden in vielen Stellen aufgerufen. Beispielsweise lesen und schreiben Array-Methoden auf Arrays durch diese internen Methoden, sodass Methoden wie push()
auch get()
- und set()
-Traps aufrufen würden.
Die meisten internen Methoden sind in dem, was sie tun, recht einfach. Die einzigen beiden, die möglicherweise verwirrender sind, sind [[Set]]
und [[DefineOwnProperty]]
. Für normale Objekte ruft die erstere Setter auf, die letztere nicht. (Und [[Set]]
ruft [[DefineOwnProperty]]
intern auf, wenn keine vorhandene Eigenschaft vorhanden ist oder die Eigenschaft eine Dateneigenschaft ist.) Während Sie vielleicht wissen, dass die obj.x = 1
-Syntax [[Set]]
verwendet, und Object.defineProperty()
verwendet [[DefineOwnProperty]]
, ist es nicht sofort ersichtlich, welche Semantiken andere integrierte Methoden und Syntaxen verwenden. Zum Beispiel verwenden Klassenfelder die [[DefineOwnProperty]]
-Semantik, weshalb Setter, die in der Oberklasse 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.
Beispiele
Einfaches Beispiel
In diesem Beispiel wird die Zahl 37
als Standardwert zurückgegeben, wenn der Eigenschaftsname nicht im Objekt vorhanden ist. Es wird der get()
-Handler verwendet.
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-Weiterleitungsproxy
In diesem Beispiel verwenden wir ein natives JavaScript-Objekt, auf das unser Proxy alle darauf angewendeten Operationen weiterleiten wird.
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!)
Beachten Sie, dass während dieser "No-op" für einfache JavaScript-Objekte funktioniert, er nicht für native Objekte wie DOM-Elemente, Map
-Objekte oder alles, was interne Slots hat, funktioniert. Siehe keine private Eigenschaftsweiterleitung für weitere Informationen.
Keine private Eigenschaftsweiterleitung
Ein Proxy ist immer noch ein anderes Objekt mit einer anderen Identität – er ist ein Proxy, der zwischen dem umschlossenen Objekt und der Außenwelt operiert. Daher hat der Proxy keinen direkten Zugriff auf die privaten Eigenschaften des Originalobjekts.
class Secret {
#secret;
constructor(secret) {
this.#secret = secret;
}
get secret() {
return this.#secret.replace(/\d+/, "[REDACTED]");
}
}
const aSecret = new Secret("123456");
console.log(aSecret.secret); // [REDACTED]
// Looks like a no-op forwarding...
const proxy = new Proxy(aSecret, {});
console.log(proxy.secret); // TypeError: Cannot read private member #secret from an object whose class did not declare it
Dies liegt daran, dass, wenn der get
-Trap des Proxys aufgerufen wird, der this
-Wert der proxy
statt des ursprünglichen secret
ist, sodass #secret
nicht zugänglich ist. Um dies zu beheben, verwenden Sie das ursprüngliche secret
als this
:
const proxy = new Proxy(aSecret, {
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);
Für Methoden bedeutet dies, dass Sie den this
-Wert der Methode ebenfalls auf das ursprüngliche Objekt umleiten müssen:
class Secret {
#x = 1;
x() {
return this.#x;
}
}
const aSecret = new Secret();
const proxy = new Proxy(aSecret, {
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());
Einige native JavaScript-Objekte haben Eigenschaften, sogenannte interne Slots, die aus JavaScript-Code nicht zugänglich sind. Beispielsweise haben Map
-Objekte einen internen Slot namens [[MapData]]
, der die Schlüssel-Wert-Paare der Map speichert. Daher können Sie nicht einfach einen Weiterleitungsproxy für eine Map erstellen:
const proxy = new Proxy(new Map(), {});
console.log(proxy.size); // TypeError: get size method called on incompatible Proxy
Sie müssen den oben illustrierten "this
-wiederherstellenden" Proxy verwenden, um dies zu umgehen.
Validierung
Mit einem Proxy
können Sie den übergebenen Wert für ein Objekt leicht validieren. Dieses Beispiel verwendet den set()
-Handler.
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
In diesem Beispiel verwenden wir Proxy
, um ein Attribut von zwei verschiedenen Elementen umzuschalten: Wenn wir das Attribut auf einem Element festlegen, wird das Attribut am anderen Element deaktiviert.
Wir erstellen ein view
-Objekt, das ein Proxy für ein Objekt mit einer selected
-Eigenschaft ist. Der Proxy-Handler definiert den set()
-Handler.
Wenn wir einem HTML-Element view.selected
zuweisen, wird das 'aria-selected'
-Attribut des Elements auf true
gesetzt. Wenn wir anschließend ein anderes Element view.selected
zuweisen, wird bei diesem Element das 'aria-selected'
-Attribut auf true
gesetzt und das 'aria-selected'
-Attribut des vorherigen Elements automatisch auf false
.
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
Wertkorrektur und eine zusätzliche Eigenschaft
Das products
-Proxy-Objekt bewertet den übergebenen Wert und wandelt ihn bei Bedarf in ein Array um. Das Objekt unterstützt auch eine zusätzliche Eigenschaft namens latestBrowser
sowohl als Getter als auch als Setter.
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'
Ein vollständiges Beispiel für eine Traps-Liste
Um nun ein vollständiges Beispiel für eine traps
-Liste zu erstellen, versuchen wir, ein nicht-natives Objekt zu proxifizieren, das besonders für diesen Operationstyp geeignet ist: das globale docCookies
-Objekt, das von einem einfachen Cookie-Framework erstellt wurde.
/*
const docCookies = ... get the "docCookies" object here:
https://reference.codeproject.com/dom/document/cookie/simple_document.cookie_framework
*/
const docCookies = new Proxy(docCookies, {
get(target, key) {
return target[key] ?? target.getItem(key) ?? undefined;
},
set(target, key, value) {
if (key in target) {
return false;
}
return target.setItem(key, value);
},
deleteProperty(target, key) {
if (!(key in target)) {
return false;
}
return target.removeItem(key);
},
ownKeys(target) {
return target.keys();
},
has(target, key) {
return key in target || target.hasItem(key);
},
defineProperty(target, key, descriptor) {
if (descriptor && "value" in descriptor) {
target.setItem(key, descriptor.value);
}
return target;
},
getOwnPropertyDescriptor(target, key) {
const value = target.getItem(key);
return value
? {
value,
writable: true,
enumerable: true,
configurable: false,
}
: undefined;
},
});
/* Cookies test */
console.log((docCookies.myCookie1 = "First value"));
console.log(docCookies.getItem("myCookie1"));
docCookies.setItem("myCookie1", "Changed value");
console.log(docCookies.myCookie1);
Spezifikationen
Specification |
---|
ECMAScript® 2025 Language Specification # sec-proxy-objects |
Browser-Kompatibilität
BCD tables only load in the browser
Siehe auch
- Proxies sind großartig – Präsentation von Brendan Eich auf der JSConf (2014)