get, set)get, set)get)set)enumerate-Trap?Proxies ermöglichen es uns, Operationen auf Objekten abzufangen und anzupassen (z. B. das Lesen von Eigenschaften). Sie sind ein Metaprogrammierungs-Feature.
Im folgenden Beispiel
proxy ist ein leeres Objekt.
handler kann Operationen abfangen, die auf proxy ausgeführt werden, indem er bestimmte Methoden implementiert.
Wenn der Handler eine Operation nicht abfängt, wird sie an target weitergeleitet.
Wir fangen nur eine Operation ab – get (Lesen von Eigenschaften)
const logged = [];
const target = {size: 0};
const handler = {
get(target, propKey, receiver) {
logged.push('GET ' + propKey);
return 123;
}
};
const proxy = new Proxy(target, handler);Wenn wir die Eigenschaft proxy.size lesen, fängt der Handler diese Operation ab.
Siehe die Referenz für die vollständige API für eine Liste der abfangbaren Operationen.
Bevor wir uns damit beschäftigen, was Proxies sind und warum sie nützlich sind, müssen wir zunächst verstehen, was Metaprogrammierung ist.
In der Programmierung gibt es Ebenen.
Basis- und Metaebene können unterschiedliche Sprachen sein. Im folgenden Beispielprogramm ist die Metaprogrammiersprache JavaScript und die Basisprogrammiersprache Java.
Metaprogrammierung kann verschiedene Formen annehmen. Im vorherigen Beispiel haben wir Java-Code auf die Konsole ausgegeben. Verwenden wir JavaScript sowohl als Metaprogrammiersprache als auch als Basisprogrammiersprache. Das klassische Beispiel hierfür ist die Funktion eval(), mit der wir JavaScript-Code im laufenden Betrieb auswerten/kompilieren können. In der folgenden Interaktion verwenden wir sie, um den Ausdruck 5 + 2 auszuwerten.
Andere JavaScript-Operationen sehen vielleicht nicht wie Metaprogrammierung aus, sind es aber tatsächlich, wenn wir genauer hinsehen.
// Base level
const obj = {
hello() {
console.log('Hello!');
},
};
// Meta level
for (const key of Object.keys(obj)) {
console.log(key);
}Das Programm untersucht seine eigene Struktur während der Ausführung. Das sieht nicht nach Metaprogrammierung aus, da die Trennung zwischen Programmierkonstrukten und Datenstrukturen in JavaScript verschwommen ist. Alle Object.*-Methoden können als Metaprogrammierungsfunktionalität betrachtet werden.
Reflektive Metaprogrammierung bedeutet, dass ein Programm sich selbst verarbeitet. Kiczales et al. [2] unterscheiden drei Arten der reflektiven Metaprogrammierung:
Betrachten wir Beispiele.
Beispiel: Introspektion. Object.keys() führt Introspektion durch (siehe vorheriges Beispiel).
Beispiel: Selbstmodifikation. Die folgende Funktion moveProperty verschiebt eine Eigenschaft von einer Quelle zu einem Ziel. Sie führt Selbstmodifikation durch den Klammeroperator für den Eigenschaftszugriff, den Zuweisungsoperator und den delete-Operator durch. (Im Produktionscode würden wir für diese Aufgabe wahrscheinlich Eigenschaftsdeskriptoren verwenden.)
function moveProperty(source, propertyName, target) {
target[propertyName] = source[propertyName];
delete source[propertyName];
}So wird moveProperty() verwendet.
const obj1 = { color: 'blue' };
const obj2 = {};
moveProperty(obj1, 'color', obj2);
assert.deepEqual(
obj1, {});
assert.deepEqual(
obj2, { color: 'blue' });ECMAScript 5 unterstützt keine Interzession; Proxies wurden geschaffen, um diese Lücke zu schließen.
Proxies bringen Interzession nach JavaScript. Sie funktionieren wie folgt. Es gibt viele Operationen, die wir auf einem Objekt obj ausführen können – zum Beispiel:
prop eines Objekts obj (obj.prop)obj eine Eigenschaft prop hat ('prop' in obj)Proxies sind spezielle Objekte, die es uns ermöglichen, einige dieser Operationen anzupassen. Ein Proxy wird mit zwei Parametern erstellt:
handler: Für jede Operation gibt es eine entsprechende Handler-Methode, die – falls vorhanden – diese Operation ausführt. Eine solche Methode fängt die Operation ab (auf dem Weg zum Target) und wird als Trap bezeichnet – ein Begriff aus dem Bereich der Betriebssysteme.target: Wenn der Handler eine Operation nicht abfängt, wird sie auf dem Target ausgeführt. Das heißt, er dient als Fallback für den Handler. In gewisser Weise umschließt der Proxy das Target.Hinweis: Die Verbform von „Interzession“ ist „interzedieren“. Interzession ist bidirektional. Abfangen ist unidirektional.
Im folgenden Beispiel fängt der Handler die Operationen get und has ab.
const logged = [];
const target = {};
const handler = {
/** Intercepts: getting properties */
get(target, propKey, receiver) {
logged.push(`GET ${propKey}`);
return 123;
},
/** Intercepts: checking whether properties exist */
has(target, propKey) {
logged.push(`HAS ${propKey}`);
return true;
}
};
const proxy = new Proxy(target, handler);Wenn wir eine Eigenschaft lesen (Zeile A) oder den in-Operator verwenden (Zeile B), fängt der Handler diese Operationen ab.
assert.equal(proxy.age, 123); // (A)
assert.equal('hello' in proxy, true); // (B)
assert.deepEqual(
logged, [
'GET age',
'HAS hello',
]);Der Handler implementiert den Trap set (Schreiben von Eigenschaften) nicht. Daher wird das Setzen von proxy.age an target weitergeleitet und führt dazu, dass target.age gesetzt wird.
Wenn das Target eine Funktion ist, können zwei zusätzliche Operationen abgefangen werden:
apply: Ausführen eines Funktionsaufrufs. Ausgelöst durchproxy(···)proxy.call(···)proxy.apply(···)construct: Ausführen eines Konstruktoraufrufs. Ausgelöst durchnew proxy(···)Der Grund, warum diese Traps nur für Funktionsziele aktiviert sind, ist einfach: Andernfalls könnten wir die Operationen apply und construct nicht weiterleiten.
Wenn wir Methodenaufrufe über einen Proxy abfangen wollen, stehen wir vor einer Herausforderung: Es gibt keinen Trap für Methodenaufrufe. Stattdessen wird ein Methodenaufruf als eine Sequenz von zwei Operationen betrachtet:
get, um eine Funktion abzurufenapply, um diese Funktion aufzurufenDaher müssen wir, wenn wir Methodenaufrufe abfangen wollen, zwei Operationen abfangen:
get ab und geben eine Funktion zurück.Der folgende Code zeigt, wie das gemacht wird.
const traced = [];
function traceMethodCalls(obj) {
const handler = {
get(target, propKey, receiver) {
const origMethod = target[propKey];
return function (...args) { // implicit parameter `this`!
const result = origMethod.apply(this, args);
traced.push(propKey + JSON.stringify(args)
+ ' -> ' + JSON.stringify(result));
return result;
};
}
};
return new Proxy(obj, handler);
}Wir verwenden nicht einen Proxy für die zweite Abfangung; wir umschließen einfach die ursprüngliche Methode in einer Funktion.
Verwenden wir das folgende Objekt, um traceMethodCalls() auszuprobieren.
const obj = {
multiply(x, y) {
return x * y;
},
squared(x) {
return this.multiply(x, x);
},
};
const tracedObj = traceMethodCalls(obj);
assert.equal(
tracedObj.squared(9), 81);
assert.deepEqual(
traced, [
'multiply[9,9] -> 81',
'squared[9] -> 81',
]);Sogar der Aufruf this.multiply() innerhalb von obj.squared() wird nachverfolgt! Das liegt daran, dass this weiterhin auf den Proxy verweist.
Das ist keine die effizienteste Lösung. Man könnte zum Beispiel Methoden cachen. Außerdem haben Proxies selbst Auswirkungen auf die Leistung.
Proxies können widerrufen (abgeschaltet) werden.
Nachdem wir die Funktion revoke zum ersten Mal aufgerufen haben, führt jeder Vorgang, den wir auf proxy anwenden, zu einem TypeError. Nachfolgende Aufrufe von revoke haben keine weiteren Auswirkungen.
const target = {}; // Start with an empty object
const handler = {}; // Don’t intercept anything
const {proxy, revoke} = Proxy.revocable(target, handler);
// `proxy` works as if it were the object `target`:
proxy.city = 'Paris';
assert.equal(proxy.city, 'Paris');
revoke();
assert.throws(
() => proxy.prop,
/^TypeError: Cannot perform 'get' on a proxy that has been revoked$/
);Ein Proxy proto kann zum Prototyp eines Objekts obj werden. Einige Operationen, die in obj beginnen, werden in proto fortgesetzt. Eine solche Operation ist get.
const proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET '+propertyKey);
return target[propertyKey];
}
});
const obj = Object.create(proto);
obj.weight;
// Output:
// 'GET weight'Die Eigenschaft weight kann nicht in obj gefunden werden, weshalb die Suche in proto fortgesetzt wird und dort der Trap get ausgelöst wird. Es gibt weitere Operationen, die Prototypen beeinflussen; sie sind am Ende dieses Kapitels aufgeführt.
Operationen, deren Traps der Handler nicht implementiert, werden automatisch an das Target weitergeleitet. Manchmal gibt es eine zusätzliche Aufgabe, die wir ausführen möchten, zusätzlich zur Weiterleitung der Operation. Zum Beispiel, um alle Operationen abzufangen und zu protokollieren, ohne dass sie das Target erreichen.
const handler = {
deleteProperty(target, propKey) {
console.log('DELETE ' + propKey);
return delete target[propKey];
},
has(target, propKey) {
console.log('HAS ' + propKey);
return propKey in target;
},
// Other traps: similar
}Reflect.*Für jeden Trap protokollieren wir zuerst den Namen der Operation und leiten sie dann weiter, indem wir sie manuell ausführen. JavaScript verfügt über das modulähnliche Objekt Reflect, das bei der Weiterleitung hilft.
Für jeden Trap
Reflect hat eine Methode.
Wenn wir Reflect verwenden, sieht das vorherige Beispiel wie folgt aus.
const handler = {
deleteProperty(target, propKey) {
console.log('DELETE ' + propKey);
return Reflect.deleteProperty(target, propKey);
},
has(target, propKey) {
console.log('HAS ' + propKey);
return Reflect.has(target, propKey);
},
// Other traps: similar
}Nun ist das, was jeder der Traps tut, so ähnlich, dass wir den Handler über einen Proxy implementieren können.
const handler = new Proxy({}, {
get(target, trapName, receiver) {
// Return the handler method named trapName
return (...args) => {
console.log(trapName.toUpperCase() + ' ' + args[1]);
// Forward the operation
return Reflect[trapName](...args);
};
},
});Für jeden Trap fragt der Proxy über die get-Operation nach einer Handler-Methode und wir geben ihm eine. Das heißt, alle Handler-Methoden können über die einzelne Meta-Methode get implementiert werden. Es war eines der Ziele der Proxy-API, diese Art der Virtualisierung zu vereinfachen.
Verwenden wir diesen Proxy-basierten Handler.
const target = {};
const proxy = new Proxy(target, handler);
proxy.distance = 450; // set
assert.equal(proxy.distance, 450); // get
// Was `set` operation correctly forwarded to `target`?
assert.equal(
target.distance, 450);
// Output:
// 'SET distance'
// 'GETOWNPROPERTYDESCRIPTOR distance'
// 'DEFINEPROPERTY distance'
// 'GET distance'Ein Proxy-Objekt kann als Abfangen von Operationen auf seinem Target-Objekt betrachtet werden – der Proxy umschließt das Target. Das Handler-Objekt des Proxys ist wie ein Beobachter oder Listener für den Proxy. Es gibt an, welche Operationen abgefangen werden sollen, indem entsprechende Methoden (get zum Lesen einer Eigenschaft usw.) implementiert werden. Wenn die Handler-Methode für eine Operation fehlt, wird diese Operation nicht abgefangen. Sie wird einfach an das Target weitergeleitet.
Daher sollte der Proxy das Target transparent umschließen, wenn der Handler das leere Objekt ist. Leider funktioniert das nicht immer.
thisBevor wir tiefer eintauchen, wollen wir kurz wiederholen, wie das Umschließen eines Targets this beeinflusst.
const target = {
myMethod() {
return {
thisIsTarget: this === target,
thisIsProxy: this === proxy,
};
}
};
const handler = {};
const proxy = new Proxy(target, handler);Wenn wir target.myMethod() direkt aufrufen, zeigt this auf target.
Wenn wir diese Methode über den Proxy aufrufen, zeigt this auf proxy.
Das heißt, wenn der Proxy einen Methodenaufruf an das Target weiterleitet, wird this nicht geändert. Infolgedessen bleibt der Proxy im Spiel, wenn das Target this verwendet, z. B. um einen Methodenaufruf zu tätigen.
Normalerweise umschließen Proxies mit leeren Handlern Targets transparent: Wir bemerken nicht, dass sie da sind, und sie ändern das Verhalten der Targets nicht.
Wenn jedoch ein Target Informationen mit this über einen Mechanismus verknüpft, der nicht von Proxies gesteuert wird, haben wir ein Problem: Die Dinge schlagen fehl, weil unterschiedliche Informationen verknüpft sind, je nachdem, ob das Target umschlossen ist oder nicht.
Zum Beispiel speichert die folgende Klasse Person private Informationen in der WeakMap _name (mehr Informationen zu dieser Technik finden Sie in JavaScript for impatient programmers).
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}Instanzen von Person können nicht transparent umschlossen werden.
const jane = new Person('Jane');
assert.equal(jane.name, 'Jane');
const proxy = new Proxy(jane, {});
assert.equal(proxy.name, undefined);jane.name ist anders als das umschlossene proxy.name. Die folgende Implementierung hat dieses Problem nicht.
class Person2 {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
}
const jane = new Person2('Jane');
assert.equal(jane.name, 'Jane');
const proxy = new Proxy(jane, {});
assert.equal(proxy.name, 'Jane');Instanzen der meisten integrierten Konstruktoren verwenden ebenfalls einen Mechanismus, der nicht von Proxies abgefangen wird. Daher können sie ebenfalls nicht transparent umschlossen werden. Das sehen wir, wenn wir eine Instanz von Date verwenden.
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
assert.throws(
() => proxy.getFullYear(),
/^TypeError: this is not a Date object\.$/
);Der Mechanismus, der von Proxies unbeeinflusst bleibt, wird als interne Slots bezeichnet. Diese Slots sind eigenschaftsähnliche Speicher, die mit Instanzen verknüpft sind. Die Spezifikation behandelt diese Slots, als wären sie Eigenschaften mit Namen in eckigen Klammern. Zum Beispiel ist die folgende Methode intern und kann für alle Objekte O aufgerufen werden.
Im Gegensatz zu Eigenschaften wird auf interne Slots nicht über normale „get“- und „set“-Operationen zugegriffen. Wenn .getFullYear() über einen Proxy aufgerufen wird, kann es den benötigten internen Slot nicht auf this finden und beschwert sich mit einem TypeError.
Für Date-Methoden besagt die Sprachspezifikation:
Sofern nicht ausdrücklich anders angegeben, sind die unten definierten Methoden des Date-Prototyp-Objekts nicht generisch und der
this-Wert, der an sie übergeben wird, muss ein Objekt sein, das einen[[DateValue]]-internen Slot hat, der mit einem Zeitwert initialisiert wurde.
Als Umgehungslösung können wir ändern, wie der Handler Methodenaufrufe weiterleitet und this selektiv auf das Target und nicht auf den Proxy setzen.
const handler = {
get(target, propKey, receiver) {
if (propKey === 'getFullYear') {
return target.getFullYear.bind(target);
}
return Reflect.get(target, propKey, receiver);
},
};
const proxy = new Proxy(new Date('2030-12-24'), handler);
assert.equal(proxy.getFullYear(), 2030);Der Nachteil dieses Ansatzes ist, dass keine der Operationen, die die Methode auf this ausführt, über den Proxy läuft.
Im Gegensatz zu anderen integrierten Objekten können Arrays transparent umschlossen werden.
const p = new Proxy(new Array(), {});
p.push('a');
assert.equal(p.length, 1);
p.length = 0;
assert.equal(p.length, 0);Der Grund, warum Arrays umschließbar sind, ist, dass der Eigenschaftszugriff zwar angepasst wird, um .length funktionieren zu lassen, aber Array-Methoden nicht auf interne Slots angewiesen sind – sie sind generisch.
Dieser Abschnitt zeigt, wofür Proxies verwendet werden können. Das gibt uns die Gelegenheit, die API in Aktion zu sehen.
get, set)Nehmen wir an, wir haben eine Funktion tracePropertyAccesses(obj, propKeys), die protokolliert, wann eine Eigenschaft von obj, deren Schlüssel in der Array propKeys enthalten ist, gesetzt oder gelesen wird. Im folgenden Code wenden wir diese Funktion auf eine Instanz der Klasse Point an.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `Point(${this.x}, ${this.y})`;
}
}
// Trace accesses to properties `x` and `y`
const point = new Point(5, 7);
const tracedPoint = tracePropertyAccesses(point, ['x', 'y']);Das Lesen und Setzen von Eigenschaften des nachverfolgten Objekts p hat folgende Auswirkungen:
Interessanterweise funktioniert die Nachverfolgung auch dann, wenn Point auf die Eigenschaften zugreift, da this nun auf das nachverfolgte Objekt und nicht auf eine Instanz von Point verweist.
tracePropertyAccesses() ohne ProxiesOhne Proxies würden wir tracePropertyAccesses() wie folgt implementieren. Wir ersetzen jede Eigenschaft durch einen Getter und einen Setter, der die Zugriffe nachverfolgt. Die Setter und Getter verwenden ein zusätzliches Objekt, propData, um die Daten der Eigenschaften zu speichern. Beachten Sie, dass wir die ursprüngliche Implementierung destruktiv ändern, was bedeutet, dass wir Metaprogrammierung betreiben.
function tracePropertyAccesses(obj, propKeys, log=console.log) {
// Store the property data here
const propData = Object.create(null);
// Replace each property with a getter and a setter
propKeys.forEach(function (propKey) {
propData[propKey] = obj[propKey];
Object.defineProperty(obj, propKey, {
get: function () {
log('GET '+propKey);
return propData[propKey];
},
set: function (value) {
log('SET '+propKey+'='+value);
propData[propKey] = value;
},
});
});
return obj;
}Der Parameter log erleichtert das Unit-Testing dieser Funktion.
const obj = {};
const logged = [];
tracePropertyAccesses(obj, ['a', 'b'], x => logged.push(x));
obj.a = 1;
assert.equal(obj.a, 1);
obj.c = 3;
assert.equal(obj.c, 3);
assert.deepEqual(
logged, [
'SET a=1',
'GET a',
]);tracePropertyAccesses() mit einem ProxyProxies bieten uns eine einfachere Lösung. Wir fangen das Lesen und Setzen von Eigenschaften ab und müssen die Implementierung nicht ändern.
function tracePropertyAccesses(obj, propKeys, log=console.log) {
const propKeySet = new Set(propKeys);
return new Proxy(obj, {
get(target, propKey, receiver) {
if (propKeySet.has(propKey)) {
log('GET '+propKey);
}
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
if (propKeySet.has(propKey)) {
log('SET '+propKey+'='+value);
}
return Reflect.set(target, propKey, value, receiver);
},
});
}get, set)Wenn es um den Zugriff auf Eigenschaften geht, ist JavaScript sehr nachsichtig. Wenn wir beispielsweise versuchen, eine Eigenschaft zu lesen und ihren Namen falsch schreiben, erhalten wir keine Ausnahme – wir erhalten das Ergebnis undefined.
Wir können Proxies verwenden, um in einem solchen Fall eine Ausnahme zu erhalten. Das funktioniert wie folgt. Wir machen den Proxy zum Prototyp eines Objekts. Wenn eine Eigenschaft nicht im Objekt gefunden wird, wird der get-Trap des Proxys ausgelöst.
get-Operation an das Target weiterleiten (der Proxy erhält seinen Prototyp vom Target).Dies ist eine Implementierung dieses Ansatzes.
const propertyCheckerHandler = {
get(target, propKey, receiver) {
// Only check string property keys
if (typeof propKey === 'string' && !(propKey in target)) {
throw new ReferenceError('Unknown property: ' + propKey);
}
return Reflect.get(target, propKey, receiver);
}
};
const PropertyChecker = new Proxy({}, propertyCheckerHandler);Verwenden wir PropertyChecker für ein Objekt.
const jane = {
__proto__: PropertyChecker,
name: 'Jane',
};
// Own property:
assert.equal(
jane.name,
'Jane');
// Typo:
assert.throws(
() => jane.nmae,
/^ReferenceError: Unknown property: nmae$/);
// Inherited property:
assert.equal(
jane.toString(),
'[object Object]');PropertyChecker als KlasseWenn wir PropertyChecker in einen Konstruktor umwandeln, können wir ihn über extends für Klassen verwenden.
// We can’t change .prototype of classes, so we are using a function
function PropertyChecker2() {}
PropertyChecker2.prototype = new Proxy({}, propertyCheckerHandler);
class Point extends PropertyChecker2 {
constructor(x, y) {
super();
this.x = x;
this.y = y;
}
}
const point = new Point(5, 7);
assert.equal(point.x, 5);
assert.throws(
() => point.z,
/^ReferenceError: Unknown property: z/);Dies ist die Prototypenkette von point.
const p = Object.getPrototypeOf.bind(Object);
assert.equal(p(point), Point.prototype);
assert.equal(p(p(point)), PropertyChecker2.prototype);
assert.equal(p(p(p(point))), Object.prototype);Wenn wir uns Sorgen machen, versehentlich Eigenschaften zu erstellen, haben wir zwei Möglichkeiten:
set abfängt.obj nicht erweiterbar machen, indem wir Object.preventExtensions(obj) verwenden, was bedeutet, dass JavaScript uns keine neuen (eigenen) Eigenschaften zu obj hinzufügen lässt.get)Einige Array-Methoden erlauben es uns, auf das letzte Element über -1, auf das vorletzte Element über -2 usw. zu verweisen. Zum Beispiel:
Leider funktioniert das beim Zugriff auf Elemente über den Klammeroperator ([]) nicht. Wir können jedoch Proxies verwenden, um diese Funktionalität hinzuzufügen. Die folgende Funktion createArray() erstellt Arrays, die negative Indizes unterstützen. Sie tut dies, indem sie Proxies um Array-Instanzen wickelt. Die Proxies fangen die get-Operation ab, die durch den Klammeroperator ausgelöst wird.
function createArray(...elements) {
const handler = {
get(target, propKey, receiver) {
if (typeof propKey === 'string') {
const index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
}
return Reflect.get(target, propKey, receiver);
}
};
// Wrap a proxy around the Array
return new Proxy(elements, handler);
}
const arr = createArray('a', 'b', 'c');
assert.equal(
arr[-1], 'c');
assert.equal(
arr[0], 'a');
assert.equal(
arr.length, 3);set)Datenbindung befasst sich mit der Synchronisierung von Daten zwischen Objekten. Ein beliebter Anwendungsfall sind Widgets, die auf dem MVC (Model View Controller)-Muster basieren: Bei der Datenbindung bleibt die Ansicht (das Widget) aktuell, wenn wir das Modell (die vom Widget visualisierten Daten) ändern.
Um Datenbindung zu implementieren, müssen wir Änderungen an einem Objekt beobachten und darauf reagieren. Das folgende Code-Snippet ist eine Skizze, wie die Beobachtung von Änderungen für Arrays aussehen könnte.
function createObservedArray(callback) {
const array = [];
return new Proxy(array, {
set(target, propertyKey, value, receiver) {
callback(propertyKey, value);
return Reflect.set(target, propertyKey, value, receiver);
}
});
}
const observedArray = createObservedArray(
(key, value) => console.log(
`${JSON.stringify(key)} = ${JSON.stringify(value)}`));
observedArray.push('a');
// Output:
// '"0" = "a"'
// '"length" = 1'Ein Proxy kann verwendet werden, um ein Objekt zu erstellen, auf dem beliebige Methoden aufgerufen werden können. Im folgenden Beispiel erstellt die Funktion createWebService() ein solches Objekt, service. Das Aufrufen einer Methode auf service ruft den Inhalt der Ressource des Webservices mit demselben Namen ab. Der Abruf erfolgt über ein Promise.
const service = createWebService('http://example.com/data');
// Read JSON data in http://example.com/data/employees
service.employees().then((jsonStr) => {
const employees = JSON.parse(jsonStr);
// ···
});Der folgende Code ist eine schnelle und schmutzige Implementierung von createWebService ohne Proxies. Wir müssen im Voraus wissen, welche Methoden auf service aufgerufen werden. Der Parameter propKeys liefert uns diese Information; er enthält ein Array mit Methodennamen.
function createWebService(baseUrl, propKeys) {
const service = {};
for (const propKey of propKeys) {
service[propKey] = () => {
return httpGet(baseUrl + '/' + propKey);
};
}
return service;
}Mit Proxies ist createWebService() einfacher.
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
// Return the method to be called
return () => httpGet(baseUrl + '/' + propKey);
}
});
}Beide Implementierungen verwenden die folgende Funktion, um HTTP-GET-Anfragen zu stellen (wie sie funktioniert, wird in JavaScript for impatient programmers erklärt).
function httpGet(url) {
return new Promise(
(resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.responseText); // (A)
} else {
// Something went wrong (404, etc.)
reject(new Error(xhr.statusText)); // (B)
}
}
xhr.onerror = () => {
reject(new Error('Network error')); // (C)
};
xhr.open('GET', url);
xhr.send();
});
}Widerrufbare Referenzen funktionieren wie folgt: Ein Client darf nicht direkt auf eine wichtige Ressource (ein Objekt) zugreifen, sondern nur über eine Referenz (ein Zwischenobjekt, einen Wrapper um die Ressource). Normalerweise wird jede Operation, die auf die Referenz angewendet wird, an die Ressource weitergeleitet. Nachdem der Client fertig ist, wird die Ressource durch Widerrufen der Referenz, also durch Abschalten, geschützt. Von da an lösen die Anwendung von Operationen auf die Referenz Ausnahmen aus und nichts wird mehr weitergeleitet.
Im folgenden Beispiel erstellen wir eine widerrufbare Referenz für eine Ressource. Dann lesen wir eine der Eigenschaften der Ressource über die Referenz. Das funktioniert, da die Referenz uns den Zugriff gewährt. Als Nächstes widerrufen wir die Referenz. Jetzt lässt uns die Referenz die Eigenschaft nicht mehr lesen.
const resource = { x: 11, y: 8 };
const {reference, revoke} = createRevocableReference(resource);
// Access granted
assert.equal(reference.x, 11);
revoke();
// Access denied
assert.throws(
() => reference.x,
/^TypeError: Cannot perform 'get' on a proxy that has been revoked/
);Proxies sind ideal geeignet für die Implementierung widerruflicher Referenzen, da sie Operationen abfangen und weiterleiten können. Dies ist eine einfache Proxy-basierte Implementierung von createRevocableReference.
function createRevocableReference(target) {
let enabled = true;
return {
reference: new Proxy(target, {
get(target, propKey, receiver) {
if (!enabled) {
throw new TypeError(
`Cannot perform 'get' on a proxy that has been revoked`);
}
return Reflect.get(target, propKey, receiver);
},
has(target, propKey) {
if (!enabled) {
throw new TypeError(
`Cannot perform 'has' on a proxy that has been revoked`);
}
return Reflect.has(target, propKey);
},
// (Remaining methods omitted)
}),
revoke: () => {
enabled = false;
},
};
}Der Code kann durch die Proxy-als-Handler-Technik aus dem vorherigen Abschnitt vereinfacht werden. Dieses Mal ist der Handler im Grunde das Reflect-Objekt. Somit gibt der get-Trap normalerweise die entsprechende Reflect-Methode zurück. Wenn die Referenz widerrufen wurde, wird stattdessen ein TypeError ausgelöst.
function createRevocableReference(target) {
let enabled = true;
const handler = new Proxy({}, {
get(_handlerTarget, trapName, receiver) {
if (!enabled) {
throw new TypeError(
`Cannot perform '${trapName}' on a proxy`
+ ` that has been revoked`);
}
return Reflect[trapName];
}
});
return {
reference: new Proxy(target, handler),
revoke: () => {
enabled = false;
},
};
}Wir müssen widerrufbare Referenzen jedoch nicht selbst implementieren, da Proxies widerrufen werden können. Dieses Mal erfolgt der Widerruf im Proxy, nicht im Handler. Der Handler muss lediglich jede Operation an das Target weiterleiten. Wie wir gesehen haben, geschieht das automatisch, wenn der Handler keine Traps implementiert.
function createRevocableReference(target) {
const handler = {}; // forward everything
const { proxy, revoke } = Proxy.revocable(target, handler);
return { reference: proxy, revoke };
}Membranen bauen auf der Idee der widerruflicher Referenzen auf: Bibliotheken zum sicheren Ausführen von nicht vertrauenswürdigem Code umschließen diesen Code mit einer Membran, um ihn zu isolieren und den Rest des Systems sicher zu halten. Objekte passieren die Membran in zwei Richtungen:
In beiden Fällen werden widerrufbare Referenzen um die Objekte gewickelt. Von gewickelten Funktionen oder Methoden zurückgegebene Objekte werden ebenfalls gewickelt. Zusätzlich wird, wenn ein gewickeltes nasses Objekt zurück in eine Membran übergeben wird, dieses entpackt.
Sobald der nicht vertrauenswürdige Code fertig ist, werden alle widerrufbaren Referenzen widerrufen. Infolgedessen kann kein Code von außen mehr ausgeführt werden, und externe Objekte, auf die er verweist, funktionieren ebenfalls nicht mehr. Der Caja Compiler ist „ein Werkzeug, um Drittanbieter-HTML, CSS und JavaScript sicher in Ihre Website einzubetten“. Er verwendet Membranen, um dieses Ziel zu erreichen.
Das Document Object Model (DOM) der Browser wird normalerweise als Mischung aus JavaScript und C++ implementiert. Die Implementierung in reinem JavaScript ist nützlich für:
Leider kann das Standard-DOM Dinge tun, die in JavaScript nicht einfach nachgebildet werden können. Zum Beispiel sind die meisten DOM-Kollektionen Live-Ansichten auf den aktuellen Zustand des DOM, die sich dynamisch ändern, wenn sich das DOM ändert. Infolgedessen sind reine JavaScript-Implementierungen des DOM nicht sehr effizient. Einer der Gründe für die Hinzufügung von Proxies zu JavaScript war, effizientere DOM-Implementierungen zu ermöglichen.
Es gibt weitere Anwendungsfälle für Proxies. Zum Beispiel:
Remoting: Lokale Platzhalterobjekte leiten Methodenaufrufe an entfernte Objekte weiter. Dieser Anwendungsfall ähnelt dem Beispiel mit dem Webservice.
Datenzugriffsobjekte für Datenbanken: Das Lesen und Schreiben auf das Objekt liest und schreibt in die Datenbank. Dieser Anwendungsfall ähnelt dem Beispiel mit dem Webservice.
Profiling: Abfangen von Methodenaufrufen, um zu verfolgen, wie viel Zeit in jeder Methode verbracht wird. Dieser Anwendungsfall ähnelt dem Nachverfolgungsbeispiel.
Immer (von Michel Weststrate) hilft bei der nicht-destruktiven Aktualisierung von Daten. Die anzuwendenden Änderungen werden durch Aufrufen von Methoden, Setzen von Eigenschaften, Setzen von Array-Elementen usw. eines (möglicherweise verschachtelten) Entwurfszustands spezifiziert. Entwurfszustände werden über Proxies implementiert.
MobX ermöglicht es Ihnen, Änderungen an Datenstrukturen wie Objekten, Arrays und Klasseninstanzen zu beobachten. Das wird über Proxies implementiert.
Alpine.js (von Caleb Porzio) ist eine Frontend-Bibliothek, die Datenbindung über Proxies implementiert.
on-change (von Sindre Sorhus) beobachtet Änderungen an einem Objekt (über Proxies) und meldet sie.
Env utility (von Nicholas C. Zakas) ermöglicht den Zugriff auf Umgebungsvariablen über Eigenschaften und löst Ausnahmen aus, wenn sie nicht vorhanden sind. Das wird über Proxies implementiert.
LDflex (von Ruben Verborgh und Ruben Taelman) bietet eine Abfragesprache für Linked Data (denken Sie an das Semantische Web). Die flüssige Abfrage-API wird über Proxies implementiert.
In diesem Abschnitt gehen wir tiefer darauf ein, wie Proxies funktionieren und warum sie so funktionieren.
Firefox unterstützte eine Weile lang eine eingeschränkte Form der interzedierenden Metaprogrammierung: Wenn ein Objekt O eine Methode namens __noSuchMethod__ hatte, wurde es benachrichtigt, wenn eine Methode auf O aufgerufen wurde, die nicht existierte. Der folgende Code zeigt, wie das funktionierte.
const calc = {
__noSuchMethod__: function (methodName, args) {
switch (methodName) {
case 'plus':
return args.reduce((a, b) => a + b);
case 'times':
return args.reduce((a, b) => a * b);
default:
throw new TypeError('Unsupported: ' + methodName);
}
}
};
// All of the following method calls are implemented via
// .__noSuchMethod__().
assert.equal(
calc.plus(3, 5, 2), 10);
assert.equal(
calc.times(2, 3, 4), 24);
assert.equal(
calc.plus('Parts', ' of ', 'a', ' string'),
'Parts of a string');Somit funktioniert __noSuchMethod__ ähnlich wie ein Proxy-Trap. Im Gegensatz zu Proxies ist der Trap eine eigene oder geerbte Methode des Objekts, dessen Operationen wir abfangen wollen. Das Problem bei diesem Ansatz ist, dass die Basisebene (normale Methoden) und die Metaebene (__noSuchMethod__) vermischt sind. Basisebenen-Code kann versehentlich eine Meta-Ebene-Methode aufrufen oder sehen und es besteht die Möglichkeit, versehentlich eine Meta-Ebene-Methode zu definieren.
Selbst in Standard-ECMAScript werden Basis- und Metaebene manchmal vermischt. Zum Beispiel können die folgenden Metaprogrammierungsmechanismen fehlschlagen, da sie sich auf der Basisebene befinden.
obj.hasOwnProperty(propKey): Dieser Aufruf kann fehlschlagen, wenn eine Eigenschaft in der Prototypenkette die integrierte Implementierung überschreibt. Zum Beispiel schlägt obj im folgenden Code fehl.
const obj = { hasOwnProperty: null };
assert.throws(
() => obj.hasOwnProperty('width'),
/^TypeError: obj.hasOwnProperty is not a function/
);Dies sind sichere Möglichkeiten, .hasOwnProperty() aufzurufen.
func.call(···), func.apply(···): Für beide Methoden sind Problem und Lösung dieselben wie bei .hasOwnProperty().
obj.__proto__: In einfachen Objekten ist __proto__ eine spezielle Eigenschaft, mit der wir den Prototyp des Empfängers abrufen und setzen können. Daher müssen wir bei der Verwendung von einfachen Objekten als Wörterbücher die Verwendung von __proto__ als Eigenschaftsschlüssel vermeiden.
Bis jetzt sollte es offensichtlich sein, dass es problematisch ist, (Basis-Ebene) Eigenschaftsschlüssel speziell zu behandeln. Daher sind Proxies geschichtet: Basis-Ebene (das Proxy-Objekt) und Meta-Ebene (das Handler-Objekt) sind getrennt.
Proxies werden in zwei Rollen verwendet:
Als Wrapper umschließen sie ihre Targets, sie kontrollieren den Zugriff auf sie. Beispiele für Wrapper sind: widerrufbare Ressourcen und Nachverfolgung über Proxies.
Als virtuelle Objekte sind sie einfach Objekte mit speziellem Verhalten und ihre Targets spielen keine Rolle. Ein Beispiel ist ein Proxy, der Methodenaufrufe an ein entferntes Objekt weiterleitet.
Ein früheres Design der Proxy-API sah Proxies als rein virtuelle Objekte vor. Es stellte sich jedoch heraus, dass selbst in dieser Rolle ein Target nützlich war, um Invarianten (die später erklärt werden) durchzusetzen und als Fallback für Traps zu dienen, die der Handler nicht implementiert.
Proxies sind auf zwei Arten abgeschirmt:
Beide Prinzipien verleihen Proxies erhebliche Macht, andere Objekte zu imitieren. Ein Grund für die Durchsetzung von Invarianten (wie später erklärt) ist es, diese Macht im Zaum zu halten.
Wenn wir eine Möglichkeit benötigen, Proxies von Nicht-Proxies zu unterscheiden, müssen wir sie selbst implementieren. Der folgende Code ist ein Modul lib.mjs, das zwei Funktionen exportiert: eine davon erstellt Proxies, die andere bestimmt, ob ein Objekt eines dieser Proxies ist.
// lib.mjs
const proxies = new WeakSet();
export function createProxy(obj) {
const handler = {};
const proxy = new Proxy(obj, handler);
proxies.add(proxy);
return proxy;
}
export function isProxy(obj) {
return proxies.has(obj);
}Dieses Modul verwendet die Datenstruktur WeakSet, um Proxies zu verfolgen. WeakSet ist für diesen Zweck ideal geeignet, da es seine Elemente nicht davon abhält, vom Garbage Collector eingesammelt zu werden.
Das nächste Beispiel zeigt, wie lib.mjs verwendet werden kann.
// main.mjs
import { createProxy, isProxy } from './lib.mjs';
const proxy = createProxy({});
assert.equal(isProxy(proxy), true);
assert.equal(isProxy({}), false);In diesem Abschnitt untersuchen wir, wie JavaScript intern strukturiert ist und wie die Menge der Proxy-Traps ausgewählt wurde.
Im Kontext von Programmiersprachen und API-Design ist ein Protokoll eine Menge von Schnittstellen plus Regeln für deren Verwendung. Die ECMAScript-Spezifikation beschreibt, wie JavaScript-Code ausgeführt wird. Sie enthält ein Protokoll zur Behandlung von Objekten. Dieses Protokoll operiert auf einer Metaebene und wird manchmal als Metaobjektprotokoll (MOP) bezeichnet. Das JavaScript MOP besteht aus eigenen internen Methoden, die alle Objekte besitzen. „Intern“ bedeutet, dass sie nur in der Spezifikation existieren (JavaScript-Engines mögen sie haben oder nicht) und nicht aus JavaScript zugänglich sind. Die Namen interner Methoden werden in doppelten eckigen Klammern geschrieben.
Die interne Methode zum Abrufen von Eigenschaften wird .[[Get]]() genannt. Wenn wir doppelte Unterstriche anstelle von doppelten Klammern verwenden, wäre diese Methode in JavaScript ungefähr wie folgt implementiert.
// Method definition
__Get__(propKey, receiver) {
const desc = this.__GetOwnProperty__(propKey);
if (desc === undefined) {
const parent = this.__GetPrototypeOf__();
if (parent === null) return undefined;
return parent.__Get__(propKey, receiver); // (A)
}
if ('value' in desc) {
return desc.value;
}
const getter = desc.get;
if (getter === undefined) return undefined;
return getter.__Call__(receiver, []);
}Die in diesem Code aufgerufenen MOP-Methoden sind
[[GetOwnProperty]] (Trap getOwnPropertyDescriptor)[[GetPrototypeOf]] (Trap getPrototypeOf)[[Get]] (Trap get)[[Call]] (Trap apply)In Zeile A sehen wir, warum Proxys in einer Prototypenkette von get erfahren, wenn eine Eigenschaft in einem „früheren“ Objekt nicht gefunden wird: Wenn keine eigene Eigenschaft mit dem Schlüssel propKey vorhanden ist, wird die Suche im Prototyp parent von this fortgesetzt.
Grundlegende versus abgeleitete Operationen. Wir können sehen, dass .[[Get]]() andere MOP-Operationen aufruft. Operationen, die dies tun, werden als abgeleitet bezeichnet. Operationen, die nicht von anderen Operationen abhängen, werden als grundlegend bezeichnet.
Das Metaobjektprotokoll von Proxys unterscheidet sich von dem normaler Objekte. Bei normalen Objekten rufen abgeleitete Operationen andere Operationen auf. Bei Proxys wird jede Operation (unabhängig davon, ob sie grundlegend oder abgeleitet ist) entweder von einer Handler-Methode abgefangen oder an das Ziel weitergeleitet.
Welche Operationen sollten über Proxys abfangbar sein?
Der Vorteil der letzteren Vorgehensweise ist, dass sie die Leistung erhöht und bequemer ist. Wenn es beispielsweise keine Falle für get gäbe, müssten wir deren Funktionalität über getOwnPropertyDescriptor implementieren.
Ein Nachteil der Einbeziehung abgeleiteter Fallen ist, dass dies zu inkonsistentem Verhalten von Proxys führen kann. Beispielsweise kann get einen anderen Wert zurückgeben als der Wert im Deskriptor, der von getOwnPropertyDescriptor zurückgegeben wird.
Das Abfangen durch Proxys ist selektiv: Wir können nicht jede Sprachoperation abfangen. Warum wurden einige Operationen ausgeschlossen? Betrachten wir zwei Gründe.
Erstens sind stabile Operationen nicht gut für das Abfangen geeignet. Eine Operation ist stabil, wenn sie für dieselben Argumente immer dieselben Ergebnisse liefert. Wenn ein Proxy eine stabile Operation abfangen kann, kann er instabil und damit unzuverlässig werden. Strikte Gleichheit (===) ist eine solche stabile Operation. Sie kann nicht abgefangen werden und ihr Ergebnis wird berechnet, indem der Proxy selbst als ein weiteres Objekt behandelt wird. Eine andere Möglichkeit, die Stabilität zu wahren, besteht darin, eine Operation auf das Ziel anstatt auf den Proxy anzuwenden. Wie später erläutert wird, wenn wir uns ansehen, wie Invarianten für Proxys erzwungen werden, geschieht dies, wenn Object.getPrototypeOf() auf einen Proxy angewendet wird, dessen Ziel nicht erweiterbar ist.
Ein zweiter Grund, nicht mehr Operationen abfangbar zu machen, ist, dass das Abfangen die Ausführung von benutzerdefiniertem Code in Situationen bedeutet, in denen dies normalerweise nicht möglich ist. Je mehr dieses Ineinandergreifen von Code stattfindet, desto schwieriger ist es, ein Programm zu verstehen und zu debuggen. Es beeinträchtigt auch die Leistung negativ.
get versus invokeWenn wir virtuelle Methoden über Proxys erstellen möchten, müssen wir Funktionen aus einer get-Falle zurückgeben. Das wirft die Frage auf: Warum nicht eine zusätzliche Falle für Methodenaufrufe einführen (z. B. invoke)? Das würde es uns ermöglichen, zu unterscheiden zwischen
obj.prop (Trap get)obj.prop() (Trap invoke)Dafür gibt es zwei Gründe.
Erstens unterscheiden nicht alle Implementierungen zwischen get und invoke. Beispielsweise Apple's JavaScriptCore tut dies nicht.
Zweitens sollte das Extrahieren einer Methode und deren späterer Aufruf über .call() oder .apply() denselben Effekt haben wie der Aufruf der Methode über Dispatch. Mit anderen Worten, die folgenden beiden Varianten sollten äquivalent funktionieren. Wenn es eine zusätzliche Falle invoke gäbe, wäre diese Äquivalenz schwerer aufrechtzuerhalten.
// Variant 1: call via dynamic dispatch
const result1 = obj.m();
// Variant 2: extract and call directly
const m = obj.m;
const result2 = m.call(obj);invokeEinige Dinge können nur getan werden, wenn wir zwischen get und invoke unterscheiden können. Diese Dinge sind daher mit der aktuellen Proxy-API unmöglich. Zwei Beispiele sind: Auto-Binding und das Abfangen fehlender Methoden. Untersuchen wir, wie sie implementiert würden, wenn Proxys invoke unterstützen würden.
Auto-Binding. Indem wir einen Proxy zum Prototyp eines Objekts obj machen, können wir Methoden automatisch binden
m über obj.m gibt eine Funktion zurück, deren this an obj gebunden ist.obj.m() führt einen Methodenaufruf durch.Auto-Binding hilft bei der Verwendung von Methoden als Callbacks. Beispielsweise wird Variante 2 aus dem vorherigen Beispiel einfacher
Abfangen fehlender Methoden. invoke ermöglicht es einem Proxy, den zuvor erwähnten __noSuchMethod__-Mechanismus zu emulieren. Der Proxy wäre wieder der Prototyp eines Objekts obj. Er würde unterschiedlich reagieren, je nachdem, wie auf eine unbekannte Eigenschaft prop zugegriffen wird
obj.prop lesen, erfolgt kein Abfangen und undefined wird zurückgegeben.obj.prop() ausführen, fängt der Proxy ab und benachrichtigt beispielsweise einen Callback.Bevor wir uns ansehen, was Invarianten sind und wie sie für Proxys erzwungen werden, wollen wir überprüfen, wie Objekte durch Nicht-Erweiterbarkeit und Nicht-Konfigurierbarkeit geschützt werden können.
Es gibt zwei Möglichkeiten, Objekte zu schützen
Nicht-Erweiterbarkeit schützt Objekte: Wenn ein Objekt nicht erweiterbar ist, können wir keine Eigenschaften hinzufügen und seinen Prototyp nicht ändern.
Nicht-Konfigurierbarkeit schützt Eigenschaften (oder besser gesagt, ihre Attribute)
writable steuert, ob der Wert einer Eigenschaft geändert werden kann.configurable steuert, ob die Attribute einer Eigenschaft geändert werden können.Weitere Informationen zu diesem Thema finden Sie unter §10 „Schutz von Objekten vor Änderungen“.
Traditionell sind Nicht-Erweiterbarkeit und Nicht-Konfigurierbarkeit
Diese und andere Eigenschaften, die angesichts von Sprachoperationen unverändert bleiben, werden als Invarianten bezeichnet. Es ist einfach, Invarianten über Proxys zu verletzen, da sie nicht intrinsisch durch Nicht-Erweiterbarkeit usw. gebunden sind. Die Proxy-API verhindert dies, indem sie das Zielobjekt und die Ergebnisse von Handler-Methoden prüft.
Die nächsten beiden Unterabschnitte beschreiben vier Invarianten. Eine vollständige Liste der Invarianten finden Sie am Ende dieses Kapitels.
Die folgenden beiden Invarianten beinhalten Nicht-Erweiterbarkeit und Nicht-Konfigurierbarkeit. Diese werden durch die Verwendung des Zielobjekts zur Buchführung erzwungen: Die von Handler-Methoden zurückgegebenen Ergebnisse müssen weitgehend mit dem Zielobjekt synchron sein.
Object.preventExtensions(obj) true zurückgibt, müssen alle zukünftigen Aufrufe false zurückgeben und obj muss nun nicht erweiterbar sein.TypeError ausgelöst wird, wenn der Handler true zurückgibt, das Zielobjekt jedoch nicht erweiterbar ist.Object.isExtensible(obj) immer false zurückgeben.TypeError ausgelöst wird, wenn das vom Handler zurückgegebene Ergebnis nicht dasselbe ist (nach Konvertierung) wie Object.isExtensible(target).Die folgenden beiden Invarianten werden durch Prüfung von Rückgabewerten erzwungen
Object.isExtensible(obj) muss einen booleschen Wert zurückgeben.Object.getOwnPropertyDescriptor(obj, ···) muss ein Objekt oder undefined zurückgeben.TypeError ausgelöst wird, wenn der Handler keinen geeigneten Wert zurückgibt.Das Erzwingen von Invarianten hat folgende Vorteile
Die nächsten beiden Abschnitte geben Beispiele für das Erzwingen von Invarianten.
Als Reaktion auf die getPrototypeOf-Falle muss der Proxy den Prototyp des Ziels zurückgeben, wenn das Ziel nicht erweiterbar ist.
Um diese Invariante zu demonstrieren, erstellen wir einen Handler, der einen anderen Prototyp als den des Ziels zurückgibt
Das Fälschen des Prototyps funktioniert, wenn das Ziel erweiterbar ist
const extensibleTarget = {};
const extProxy = new Proxy(extensibleTarget, handler);
assert.equal(
Object.getPrototypeOf(extProxy), fakeProto);Wir erhalten jedoch einen Fehler, wenn wir den Prototyp für ein nicht erweiterbares Objekt fälschen.
const nonExtensibleTarget = {};
Object.preventExtensions(nonExtensibleTarget);
const nonExtProxy = new Proxy(nonExtensibleTarget, handler);
assert.throws(
() => Object.getPrototypeOf(nonExtProxy),
{
name: 'TypeError',
message: "'getPrototypeOf' on proxy: proxy target is"
+ " non-extensible but the trap did not return its"
+ " actual prototype",
});Wenn das Ziel eine nicht schreibbare, nicht konfigurierbare Eigenschaft hat, muss der Handler deren Wert als Reaktion auf eine get-Falle zurückgeben. Um diese Invariante zu demonstrieren, erstellen wir einen Handler, der für Eigenschaften immer denselben Wert zurückgibt.
const handler = {
get(target, propKey) {
return 'abc';
}
};
const target = Object.defineProperties(
{}, {
manufacturer: {
value: 'Iso Autoveicoli',
writable: true,
configurable: true
},
model: {
value: 'Isetta',
writable: false,
configurable: false
},
});
const proxy = new Proxy(target, handler);Die Eigenschaft target.manufacturer ist weder nicht schreibbar noch nicht konfigurierbar, was bedeutet, dass der Handler einen anderen Wert vortäuschen darf
Die Eigenschaft target.model ist jedoch sowohl nicht schreibbar als auch nicht konfigurierbar. Daher können wir ihren Wert nicht fälschen
assert.throws(
() => proxy.model,
{
name: 'TypeError',
message: "'get' on proxy: property 'model' is a read-only and"
+ " non-configurable data property on the proxy target but"
+ " the proxy did not return its actual value (expected"
+ " 'Isetta' but got 'abc')",
});enumerate-Falle?ECMAScript 6 hatte ursprünglich eine enumerate-Falle, die durch for-in-Schleifen ausgelöst wurde. Sie wurde jedoch kürzlich entfernt, um Proxys zu vereinfachen. Reflect.enumerate() wurde ebenfalls entfernt. (Quelle: TC39-Notizen)
Dieser Abschnitt ist eine Kurzübersicht über die Proxy-API
ProxyReflectDie Referenz verwendet den folgenden benutzerdefinierten Typ
Es gibt zwei Möglichkeiten, Proxys zu erstellen
const proxy = new Proxy(target, handler)
Erstellt ein neues Proxy-Objekt mit dem angegebenen Ziel und dem angegebenen Handler.
const {proxy, revoke} = Proxy.revocable(target, handler)
Erstellt einen Proxy, der über die Funktion revoke widerrufen werden kann. revoke kann mehrmals aufgerufen werden, aber nur der erste Aufruf hat eine Wirkung und schaltet den proxy ab. Danach führt jede auf den proxy ausgeführte Operation zu einem TypeError.
Dieser Unterabschnitt erklärt, welche Fallen von Handlern implementiert werden können und welche Operationen sie auslösen. Mehrere Fallen geben boolesche Werte zurück. Bei den Fallen has und isExtensible ist der boolesche Wert das Ergebnis der Operation. Bei allen anderen Fallen gibt der boolesche Wert an, ob die Operation erfolgreich war oder nicht.
Fallen für alle Objekte
defineProperty(target, propKey, propDesc): boolean
Object.defineProperty(proxy, propKey, propDesc)deleteProperty(target, propKey): boolean
delete proxy[propKey]delete proxy.somePropget(target, propKey, receiver): any
receiver[propKey]receiver.somePropgetOwnPropertyDescriptor(target, propKey): undefined|PropDesc
Object.getOwnPropertyDescriptor(proxy, propKey)getPrototypeOf(target): null|object
Object.getPrototypeOf(proxy)has(target, propKey): boolean
propKey in proxyisExtensible(target): boolean
Object.isExtensible(proxy)ownKeys(target): Array<PropertyKey>
Object.getOwnPropertyPropertyNames(proxy) (verwendet nur String-Schlüssel)Object.getOwnPropertyPropertySymbols(proxy) (verwendet nur Symbol-Schlüssel)Object.keys(proxy) (verwendet nur aufzählbare String-Schlüssel; Aufzählbarkeit wird über Object.getOwnPropertyDescriptor geprüft)preventExtensions(target): boolean
Object.preventExtensions(proxy)set(target, propKey, value, receiver): boolean
receiver[propKey] = valuereceiver.someProp = valuesetPrototypeOf(target, proto): boolean
Object.setPrototypeOf(proxy, proto)Fallen für Funktionen (verfügbar, wenn das Ziel eine Funktion ist)
apply(target, thisArgument, argumentsList): any
proxy.apply(thisArgument, argumentsList)proxy.call(thisArgument, ...argumentsList)proxy(...argumentsList)construct(target, argumentsList, newTarget): object
new proxy(..argumentsList)Die folgenden Operationen sind grundlegend, sie verwenden keine anderen Operationen, um ihre Arbeit zu erledigen: apply, defineProperty, deleteProperty, getOwnPropertyDescriptor, getPrototypeOf, isExtensible, ownKeys, preventExtensions, setPrototypeOf
Alle anderen Operationen sind abgeleitet, sie können über grundlegende Operationen implementiert werden. Beispielsweise kann get durch Iteration über die Prototypenkette über getPrototypeOf und durch Aufruf von getOwnPropertyDescriptor für jedes Kettenmitglied implementiert werden, bis entweder eine eigene Eigenschaft gefunden wird oder die Kette endet.
Invarianten sind Sicherheitsbeschränkungen für Handler. Dieser Unterabschnitt dokumentiert, welche Invarianten von der Proxy-API erzwungen werden und wie. Wenn wir unten lesen „der Handler muss X tun“, bedeutet dies, dass ein TypeError ausgelöst wird, wenn er es nicht tut. Einige Invarianten beschränken Rückgabewerte, andere Parameter. Die Korrektheit des Rückgabewerts einer Falle wird auf zwei Arten sichergestellt
TypeError.Dies ist die vollständige Liste der erzwungenen Invarianten
apply(target, thisArgument, argumentsList): any
construct(target, argumentsList, newTarget): object
null oder ein anderer primitiver Wert).defineProperty(target, propKey, propDesc): boolean
propDesc das Attribut configurable auf false setzt, muss das Ziel eine nicht konfigurierbare eigene Eigenschaft mit dem Schlüssel propKey haben.propDesc sowohl die Attribute configurable als auch writable auf false setzt, muss das Ziel eine eigene Eigenschaft mit dem Schlüssel propKey haben, die nicht konfigurierbar und nicht schreibbar ist.propKey hat, muss propDesc mit dieser Eigenschaft kompatibel sein: Wenn wir die Zieleigenschaft mit dem Deskriptor neu definieren, darf keine Ausnahme ausgelöst werden.deleteProperty(target, propKey): boolean
propKey.propKey.get(target, propKey, receiver): any
propKey hat, muss der Handler den Wert dieser Eigenschaft zurückgeben.undefined zurückgeben.getOwnPropertyDescriptor(target, propKey): undefined|PropDesc
undefined oder ein Objekt zurückgeben.getPrototypeOf(target): null|object
null oder ein Objekt sein.has(target, propKey): boolean
isExtensible(target): boolean
target.isExtensible().ownKeys(target): Array<PropertyKey>
preventExtensions(target): boolean
target.isExtensible() false ist.set(target, propKey, value, receiver): boolean
propKey hat. In diesem Fall muss value der Wert dieser Eigenschaft sein, andernfalls wird ein TypeError ausgelöst.setPrototypeOf(target, proto): boolean
proto derselbe sein wie der Prototyp des Ziels. Andernfalls wird ein TypeError ausgelöst. Invarianten in der ECMAScript-Spezifikation
In der Spezifikation sind die Invarianten in Abschnitt „Proxy Object Internal Methods and Internal Slots“ aufgeführt.
Die folgenden Operationen normaler Objekte führen Operationen auf Objekten in der Prototypenkette durch. Wenn eines der Objekte in dieser Kette ein Proxy ist, werden seine Fallen ausgelöst. Die Spezifikation implementiert die Operationen als interne eigene Methoden (die für JavaScript-Code nicht sichtbar sind). In diesem Abschnitt tun wir jedoch so, als wären sie normale Methoden mit denselben Namen wie die Fallen. Der Parameter target wird zum Empfänger des Methodenaufrufs.
target.get(propertyKey, receiver)target keine eigene Eigenschaft mit dem angegebenen Schlüssel hat, wird get auf dem Prototyp von target aufgerufen.target.has(propertyKey)get wird has auf dem Prototyp von target aufgerufen, wenn target keine eigene Eigenschaft mit dem angegebenen Schlüssel hat.target.set(propertyKey, value, receiver)get wird set auf dem Prototyp von target aufgerufen, wenn target keine eigene Eigenschaft mit dem angegebenen Schlüssel hat.Alle anderen Operationen betreffen nur eigene Eigenschaften, sie haben keine Auswirkung auf die Prototypenkette.
Interne Operationen in der ECMAScript-Spezifikation
In der Spezifikation werden diese (und andere) Operationen im Abschnitt „Ordinary Object Internal Methods and Internal Slots“ beschrieben.
Das globale Objekt Reflect implementiert alle abfangbaren Operationen des JavaScript-Metaobjektprotokolls als Methoden. Die Namen dieser Methoden sind dieselben wie die der Handler-Methoden, was, wie wir gesehen haben, beim Weiterleiten von Operationen vom Handler zum Ziel hilfreich ist.
Reflect.apply(target, thisArgument, argumentsList): any
Ähnlich wie Function.prototype.apply().
Reflect.construct(target, argumentsList, newTarget=target): object
Der new-Operator als Funktion. target ist der aufzurufende Konstruktor, der optionale Parameter newTarget verweist auf den Konstruktor, der die aktuelle Kette von Konstruktoraufrufen gestartet hat.
Reflect.defineProperty(target, propertyKey, propDesc): boolean
Ähnlich wie Object.defineProperty().
Reflect.deleteProperty(target, propertyKey): boolean
Der delete-Operator als Funktion. Er funktioniert jedoch etwas anders: Er gibt true zurück, wenn die Eigenschaft erfolgreich gelöscht wurde oder wenn die Eigenschaft nie existierte. Er gibt false zurück, wenn die Eigenschaft nicht gelöscht werden konnte und noch existiert. Die einzige Möglichkeit, Eigenschaften vor dem Löschen zu schützen, besteht darin, sie nicht konfigurierbar zu machen. Im Sloppy-Modus gibt der delete-Operator dieselben Ergebnisse zurück. Im Strict-Modus wird jedoch ein TypeError anstelle von false ausgelöst.
Reflect.get(target, propertyKey, receiver=target): any
Eine Funktion, die Eigenschaften abruft. Der optionale Parameter receiver verweist auf das Objekt, von dem das Abrufen ausging. Er wird benötigt, wenn get einen Getter später in der Prototypenkette erreicht. Dann stellt er den Wert für this bereit.
Reflect.getOwnPropertyDescriptor(target, propertyKey): undefined|PropDesc
Dasselbe wie Object.getOwnPropertyDescriptor().
Reflect.getPrototypeOf(target): null|object
Dasselbe wie Object.getPrototypeOf().
Reflect.has(target, propertyKey): boolean
Der in-Operator als Funktion.
Reflect.isExtensible(target): boolean
Dasselbe wie Object.isExtensible().
Reflect.ownKeys(target): Array<PropertyKey>
Gibt alle eigenen Eigenschaftsschlüssel in einem Array zurück: die String-Schlüssel und Symbol-Schlüssel aller eigenen aufzählbaren und nicht aufzählbaren Eigenschaften.
Reflect.preventExtensions(target): boolean
Ähnlich wie Object.preventExtensions().
Reflect.set(target, propertyKey, value, receiver=target): boolean
Eine Funktion, die Eigenschaften setzt.
Reflect.setPrototypeOf(target, proto): boolean
Der neue Standardweg zum Setzen des Prototyps eines Objekts. Der aktuelle, nicht standardmäßige Weg, der in den meisten Engines funktioniert, ist das Setzen der speziellen Eigenschaft __proto__.
Mehrere Methoden haben boolesche Ergebnisse. Für .has() und .isExtensible() sind dies die Ergebnisse der Operation. Für die restlichen Methoden zeigen sie an, ob die Operation erfolgreich war.
Reflect neben der WeiterleitungAbgesehen von der Weiterleitung von Operationen, warum ist Reflect nützlich [4]?
Unterschiedliche Rückgabewerte: Reflect dupliziert die folgenden Methoden von Object, aber seine Methoden geben boolesche Werte zurück, die angeben, ob die Operation erfolgreich war (wobei die Object-Methoden das modifizierte Objekt zurückgeben).
Object.defineProperty(obj, propKey, propDesc): objectObject.preventExtensions(obj): objectObject.setPrototypeOf(obj, proto): objectOperatoren als Funktionen: Die folgenden Reflect-Methoden implementieren Funktionalitäten, die sonst nur über Operatoren verfügbar sind
Reflect.construct(target, argumentsList, newTarget=target): objectReflect.deleteProperty(target, propertyKey): booleanReflect.get(target, propertyKey, receiver=target): anyReflect.has(target, propertyKey): booleanReflect.set(target, propertyKey, value, receiver=target): booleanKürzere Version von apply(): Wenn wir sicher sein wollen, dass wir die Methode apply() auf einer Funktion aufrufen, können wir dies nicht über dynamische Weiterleitung tun, da die Funktion eine eigene Eigenschaft mit dem Schlüssel 'apply' haben kann.
func.apply(thisArg, argArray) // not safe
Function.prototype.apply.call(func, thisArg, argArray) // safeDie Verwendung von Reflect.apply() ist kürzer als die sichere Version
Keine Ausnahmen beim Löschen von Eigenschaften: Der delete-Operator löst im Strict-Modus aus, wenn wir versuchen, eine nicht konfigurierbare eigene Eigenschaft zu löschen. Reflect.deleteProperty() gibt in diesem Fall false zurück.
Object.* versus Reflect.*Zukünftig wird Object Operationen hosten, die für normale Anwendungen von Interesse sind, während Reflect Operationen hosten wird, die auf niedrigerer Ebene angesiedelt sind.
Damit schließen wir unseren ausführlichen Blick auf die Proxy-API. Eine Sache, die man beachten sollte, ist, dass Proxys Code verlangsamen. Das kann wichtig sein, wenn die Leistung kritisch ist.
Andererseits ist Leistung oft nicht entscheidend, und es ist schön, die Metaprogrammierungsfähigkeiten zu haben, die Proxys uns bieten.
Danksagungen
Allen Wirfs-Brock wies auf die in §18.3.7 „Fallstrick: Nicht alle Objekte können transparent von Proxys umschlossen werden“ erklärte Fallgrube hin.
Die Idee für §18.4.3 „Negative Array-Indizes (get)“ stammt aus einem Blogbeitrag von Hemanth.HM.
André Jaenisch trug zur Liste der Bibliotheken bei, die Proxys verwenden.
[1] „On the design of the ECMAScript Reflection API“ von Tom Van Cutsem und Mark Miller. Technischer Bericht, 2012. [Wichtige Quelle dieses Kapitels.]
[2] „The Art of the Metaobject Protocol“ von Gregor Kiczales, Jim des Rivieres und Daniel G. Bobrow. Buch, 1991.
[3] „Putting Metaclasses to Work: A New Dimension in Object-Oriented Programming“ von Ira R. Forman und Scott H. Danforth. Buch, 1999.
[4] „Harmony-reflect: Why should I use this library?“ von Tom Van Cutsem. [Erklärt, warum Reflect nützlich ist.]