Benutzer:Rolf b/Proxy

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Einführung

JavaScript ist eine Programmiersprache, die Vererbung bei Objekten durch Prototypen statt durch Klassen realisiert. Durch den Einsatz von Prototypen ist es oft möglich, das Verhalten eines Objekts zu erweitern, ohne das Objekt selbst zu verändern. Man nennt dies auch das "Dekorierer" Muster: Man erzeugt ein neues Objekt, das sich wie das bestehende Objekt verhält, indem es alle Eigenschaftszugriffe oder Methodenaufrufe an das bestehende Objekt delegiert und nur bei Bedarf eingreift. In Sprachen wie Java oder C++ bedeutet das einiges an Arbeit, weil man dort die Delegation jeder Eigenschaft und jeder Methode ausprogrammieren muss. Prototypen in JavaScript machen das einfacher, weil die Delegation automatisch von JavaScript übernommen wird.

Aber auch Prototypen haben Grenzen, oder können Arbeit machen. Stellen Sie sich vor, Sie möchten einen Debug-Helfer schreiben, der den Aufruf jeder Methode eines Objekts mit Parametern und Rückgabewert protokolliert. Und vielleicht auch noch die Laufzeit misst. Und das Objekt, das Sie debuggen wollen, hat 20 oder mehr Methoden. Es ist nun gleich, ob Sie Klassen- oder Prototypvererbung verwenden, sie müssen für jede Methode eine Debug-Methode schreiben, die sich um das Logging und die Laufzeitmessung für diesen Methodenaufruf kümmert.

Oder Sie programmieren ein UI Mapping, wie z.B. knockout.js, das Änderungen an einem Viewmodel-Objekt automatisch ins DOM und wieder zurück überträgt. In knockout.js bedeutet das, das man jede Eigenschaft eins Viewmodel-Objekts mittels ko.observable als "beobachtbar" erzeugen muss, um das automatische Mapping zu ermöglichen. Knockout.js ist von 2010 und hatte keine andere Möglichkeit.

Mit einem Proxy-Objekt erhalten Sie weitere Möglichkeiten. Proxys sind ein Feature, das im ECMAScript-Standard von 2015 eingeführt wurde. Es ermöglicht, jedes normale JavaScript-Objekt mit einer vorgeschalteten Instanz zu versehen, die es ermöglicht, bestimmte Standardoperationen von JavaScript zu beeinflussen.

Syntax

Details: caniuse.com

Konstruktor

var proxyObjekt = new Proxy(objekt, handler);

objekt 
Das Objekt, dem ein Proxy vorgeschaltet werden soll
handler 
Ein Objekt, dessen Methoden als Callbacks bei bestimmten internen Operationen von JavaScript aufgerufen werden.

Methoden

Methoden von Proxy.prototype

Ein Proxy ist ein exotisches Objekt und besitzt keine prototype-Eigenschaft.

Beachten Sie: Ein Proxy-Objekt hat keine eigenen Eigenschaften. Statt dessen reicht es alle Zugriffe an das Objekt weiter, das dem Konstruktor als erster Parameter übergeben wurde. Seinen Nutzen gewinnt es dadurch, dass bestimmte interne Abläufe von JavaScript auf Callback-Funktionen geleitet werden, die als Methoden auf dem Handler-Objekt definiert sind, das der Konstruktor als zweiten Parameter bekommt.

revocable Proxy

Die Methode Proxy.revocable funktioniert ähnlich wie der Proxy-Konstruktor. Sie erzeugt einen Proxy, und erzeugt dann ein weiteres Objekt mit den Eigenschaften proxy und revoke. Die proxy Eigenschaft liefert den erzeugten Proxy, und revoke ist eine Methode, die den Proxy deaktiviert. Alle Callbacks werden abgeschaltet und jeder Versuch, das Proxy-Objekt zu verwenden, läuft nach revoke() auf einen TypeError.

It's a Kind of Magic

Die Punkte, an denen man mit den Callback-Methoden des handler-Objekts ansetzen kann, werden in der ECMAScript Spezifikation „Traps“, englisch für „Falle“ genannt. Einige dieser Traps sind Ihnen, wenn Sie PHP kennen, möglicherweise unter dem Namen „magische Methoden“ bekannt. Die Namen der Fallen und der handler-Methoden sind identisch.

Alle Fallen-Methoden haben einen 1:1 Bezug zu bestimmten internen Operationen von JavaScript, die vom Sprachstandard exakt festgelegt werden. Sie können das im ECMAScript Standard unter "Internal Methods and Internal Slots" nachlesen, aber bringen Sie ein Getränk mit. Der Text ist trocken.

Verfügbare Traps

Parameter der Trap-Methoden

Die Parameter der im Folgenden beschriebenen Trap-Methoden sind sich ähnlich. Sie werden darum vorab gemeinsam beschrieben:

target
Das target-Objekt des Proxy
property
Der Name der Eigenschaft, mit der sich die Trap-Methode befasst
receiver
Das Objekt, das this war als die Trap-Methode aktiviert wurde. Nähere Erklärungen dazu folgen unter „Proxys und Vererbung“

get

Die Trap-Methode für get entspricht der internen [[Get]] Operation von JavaScript und wird aktiv, wenn der Wert einer Eigenschaft gelesen werden soll. Dabei ist es egal, ob es eine einfache Werte-Eigenschaft oder eine getter-Methode ist. Der Rückgabewert der get-Methode des Handler-Objekts wird dem Nutzer des Proxy als Wert der Eigenschaft geliefert.

Signatur: get(target, propertyName, receiver) : any

Die get-Trap ähnelt der magischen Methode __get in PHP. Sie wird bei jedem Zugriff auf eine Eigenschaft des Proxy-Objekts aufgerufen. Sie kann an Hand des übergebenen Eigenschaftsnamens einen Wert fabrizieren oder den Aufruf an das target- oder receiver-Objekt weiterleiten.

Proxy mit get-Trap
let presi = {
   familienName: "Lincoln";
   vorName: "Abraham";
};
let proxy = new Proxy(presi, {
   get(target, property, receiver) {
      console.log("Proxy intercepts get operation for " + property);
      if (property in target) {
         return target[property];     /* Funktioniert nur wenn keine Vererbung verwendet wird! */
      }
      if (property == "name") {
         return target.vorname + " " + target.familienName;
      }
      throw new ReferenceError("Zugriff auf unbekannte Eigenschaft " + property);
   }
});

Diese get-Trap protokolliert alle Zugriffe und leitet Zugriffe auf bekannte Eigenschaften des Objekts an das Objekt weiter. Ist die Eigenschaft unbekannt, prüft sie, ob die Eigenschaft "name" gelesen werden soll, und fabriziert einen Wert dafür. Andernfalls wird ein Error-Objekt erzeugt und als Exception geworfen.

Die Synthese der name-Eigenschaft hätte man auch mit einem Dekorationsobjekt erreichen können, das einen getter-Methode für die name Eigenschaft enthält. Das Werfen einer Ausnahmebedingung bei Zugriffs auf eine unbekannte Eigenschaft dagegen nicht.

Bitte beachten Sie den Hinweis auf Vererbung. Korrekterweise muss man die Property-Abfrage über den receiver-Parameter laufen lassen. Das führt aber, wenn man einfach receiver[property] verwendet, zu einer Endlos-Rekursion weil dann die get-Trap erneut aktiviert wird. Das Thema wird im Abschnitt Vererbung aufgegriffen.

set

Die Trap-Methode für set wird beim Verändern von Eigenschaften eines Objekts verwendet und entspricht der internen Methode [[Set]].

Signatur: set(target, propertyName, value, receiver) : boolean

Auch die set-Trap hat ein PHP-Gegenstück: die magische Methode __set. Unterschied zu PHP ist, dass die Trap-Methode einen boolean-Wert zurückgibt, der den Erfolg der Schreiboperation dokumentiert. Gibt die set-Methode false zurück, wirft die JavaScript-Runtime einen TypeError.

Das kann man beispielsweise dafür nutzen, eine Form von typsicherer Zuweisung zu implementieren.

Proxy mit set-Trap
let autos = [];     // Array für Auto-Objekte
let nurAutos = new Proxy(autos, {
   set(target, property, value, receiver) {
      console.log("Proxy intercepts set operation for " + property);
      if (value instanceOf Auto) {
         target[property] = value;
         return true;
      }
      else
         return false;    // Erzeugt einen TypeError
   }
});

nurAutos[0] = new Auto("rot");
nurAutos.push(new Auto("blau");
nurAutos.push("grün");        // TypeErrpr

Das Schöne ist hier, dass die Trap-Methoden tiefer verankert sind als die Propertyzugriffe. Die [[SET]] Operation ist Teil eines Schreibvorgangs auf eine Objekteigenschaft und eben auch auf Arrayeinträge, egal ob über Index oder über push.

has

Die Trap-Methode has wird aufgerufen, wenn JavaScript die interne Methode [[HasProperty]] aufruft. Dies geschieht zum Beispiel bei Verwendung des in Operators, aber auch bei vielen Array-Operationen, um festzustellen, ob an einer Indexposition ein Wert vorliegt.

Signatur: has(target, propertyName) : boolean

Es kann nützlich sein, mittels has die Existenz von Eigenschaften zu verstecken. Allerdings reicht has dafür nicht, man muss das Property auch nicht enumerierbar machen. Ist der Propertyname aber bekannt, kann auf das Property dennoch zugegriffen werden. Private Eigenschaften realisiert man besser anders.

Ein weiterer Anwendungsfall von has kann ein System mit verzögerter Initialisierung sein, bei dem eine Eigenschaft erst dann angelegt wird, wenn sie verwendet wird. Wenn die eigentliche Initialisierung in der get-Trap erfolgt und die Eigenschaft vorher nicht existiert, würde eine in Abfrage melden, dass es die Eigenschaft nicht gibt. Über die has-Trap lässt sich die Anwesenheit dieser Eigenschaft simulieren.

Eine kreative, wenn auch vielleicht fragwürdige Anwendung für die has-Trap ist eine semantische Erweiterung des in Operators. Auf javascript.info findet sich ein Vorschlag für syntaktischem Zucker dieser Art:

Range Klasse mit in Operator
let bereich = new Range(1,10);
if (5 in bereich) {
   ...
}

function Range(from, to) {
   Object.defineProperties(this, {
      from:  { enumerable: true, value: from },
      to:    { enumerable: true, value: to }});
   return new Proxy( this, {
      has(target, property) {
         let val = parseInt(property);
         return from <= val && val <= to;
      }
   });
}

Der hier gezeigte Code enthält eine Konstruktorfunktion namens Range. Sie erzeugt zwei readonly-Eigenschaften für die Unter- und Obergrenze, gibt dann aber bewusst nicht das mit new erzeugte this-Objekt zurück, sondern hüllt es in einen Proxy ein und gibt diesen zurück. Zum Proxy gehört ein handler-Objekt, das die has-Trap bedient und an Stelle der Existenz einer Eigenschaft prüft, ob der Eigenschaftsname, als Zahl genommen, zwischen den Werten von from und to liegt.

ownKeys

Es gibt verschiedene Stellen, an denen JavaScript die Eigenschaften eines Objekts auflistet. Es läuft dabei jedesmal auf die interne [[OwnPropertyKeys]] Methode, die beim Aufruf auf einem Proxy-Objekt die ownKeys Trap-Methode verwendet.

Signatur: ownKeys(target) : boolean

Allerdings bringt ein Proxy dem Ergebnis intensives Misstrauen entgegen, weil das Ergebnis von [[OwnPropertyKeys]] etliche Anforderungen erfüllen muss. Diese Anforderungen werden validiert.

Die ECMAScript-Spezifikation listet die folgenden Invarianten von [[OwnPropertyKeys]] auf:

  • Das Ergebnis ist eine Liste - das ist ein interner Typ von JavaScript. Die ownKeys Methode muss ein Array oder ein Array-artiges Objekt zurückgeben.
  • Das Ergebnis darf keine doppelten Einträge enthalten
  • Der Typ jedes der zurückgegebenen Werte ist entweder String oder Symbol
  • Das Ergebnis der Trap-Methode muss alle Eigenschaften enthalten, in deren [JavaScript/Objekte/Object/defineProperty|Property-Descriptor]] configurable auf false gesetzt wurde. Fehlt eins, wird ein TypeError geworfen.
  • Wenn das Objekt nicht erweiterbar ist (Object.isExtensible liefert false), darf das Ergebnis nur die Schlüssel derjenigen eigenen Eigenschaften enthalten, die über [[GetOwnProperty]] zugänglich sind.

Der Anwendungsfall für ownKeys kann darin liegen, bestimmte Eigenschaften des Objekts zu verbergen und auf diese Weise private Eigenschaften zu erzeugen. Die getOwnProperty... Methoden von Object liefern beispielsweise immer alle Eigenschaften, ganz gleich, ob sie als enumerable angelegt wurden oder nicht. Ein Filter in der ownKeys Trap kann dem abhelfen. Die JavaScript Spezifikation sieht für private Eigenschaften aber mittlerweile einen anderen Mechanismus vor, und der Umgang mit Proxys für private Variablen ist hakelig. Sie sollten daher von dieser Technik absehen.

Ein anderer Anwendungsfall ist die Unterstützung beim Bereitstellen von virtuellen Eigenschaften. In der get-Trap wurde gezeigt, wie ein Proxy das Auslesen einer Eigenschaft ermöglichen kann, die im Objekt gar nicht existiert. Wenn man nun möchte, dass solche Eigenschaften von JavaScript aufgelistet werden, kann man sie in der ownKeys Trap den realen Eigenschaften des Objekts hinzufügen. Allerdings reicht das noch nicht, man muss auch einen PropertyDescriptor dafür bereitstellen. Das wird im nächsten Abschnitt gezeigt.

getOwnPropertyDescriptor

Diese Trap-Methode wird aufgerufen, wenn die JavaScript-interne Methode [[GetOwnProperty]] benutzt wird. Das geschieht erstaunlich oft, zum Beispiel prüft Object.keys() die Liste der gefundenen Eigenschaftsnamen auf diesem Weg, ob sie aufzählbare Eigenschaften darstellen. Die öffentliche Methode getOwnPropertyDescriptor von Object ist ein direkter Zugang zu dieser internen Methode.

Signatur: getOwnPropertyDescriptor (target, propertyName) : descriptor

Wenn man mittels Proxy auf die PropertyDescriptor Eigenschaften eines Objekts Einfluss nehmen will, oder für virtuelle Eigenschaften PropertyDescriptoren simulieren möchte, muss man diese Trap nutzen.

Was ein PropertyDescriptor ist, wird bei Object.defineProperty erläutert. Eine wichtige Eigenschaft von Objekt-Eigenschaften ist, ob sie von Object.keys oder von einer for..in Schleife aufgelistet werden dürfen. Dafür muss im PropertyDescriptor enumerable auf true gesetzt sein. Als Beispiel soll das Präsidentenbeispiel von oben ausgebaut werden:

Proxy macht die virtuelle Eigenschaft sichtbar
let presi = {
   familienName: "Heuss";
   vorName: "Theodor";
};
let proxy = (function(obj) {
   const getName = function() { return `${this.vorName} ${this.familienName}`;  };
   const descriptors = {
      name: { enumerable: true, configurable: false, get: getName }
   };
   // Plausibilitätsprüfung
   if ("name" in obj) throw new TypeError("Objekt besitzt bereits eine name Eigenschaft");

   return new Proxy(obj, {
      // hier fehlt noch was
      ownKeys(target) {
         return [...Object.keys(target), "name"];       // Keys-Array mit Spread-Operator erweitern
      },
      getOwnPropertyDescriptor(target, propertyName) {
         if (propertyName == "name") 
            return descriptors.name;
         return Object.getOwnPropertyDescriptor(target, propertyName);
      }
   });
})(presi);

console.log(proxy.name);      // undefined?!

Hier wird - um nicht ständig temporäre Objekte anlegen zu müssen - die Proxy-Erzeugung in eine IIFE gekapselt. Darin befinden sich eine getter-Methode für die name-Eigenschaft und ein PropertyDescriptor dazu. Der eigentliche Proxy nutzt sie.

Aber es funktioniert noch nicht. Der Grund ist, dass JavaScript bei einfachen Property-Zugriffen nicht ständig die Property-Deskriptoren überprüft. Das dauert zu lange. Unser Proxy muss darum auch die get- und set-Trap setzen:

Fehlende get- und set-Trap
      // hier fehlt noch was - ersetzen mit:
      get(target, propertyName, receiver) {
         return propertyName == "name" ? getName.call(target) : target[propertyName];
      },
      set(target, propertyName, value, receiver) {
         if (propertyName == "name") return false;
         target[propertyName] = value;
         return true;
      },

console.log(proxy.name);           // Theodor Heuss
proxy.name = "Theodore Sturgeon";  // Wirft im strict mode einen Type Error und wird sonst ignoriert

Es ist sicherlich einfacher, mit defineProperty auf dem presi-Objekt einen getter für die name Eigenschaft zu definieren. Aber darum geht es bei Proxys nicht. Das eigentliche Objekt soll unverändert bleiben, und die Veränderungen allein im Proxy erfolgen.

deleteProperty

Wenn Sie den delete Operator verwenden, um eine Eigenschaft eines Objekts zu entfernen, ruft JavaScript die interne Methode Delete auf, der die deleteProperty Trap-Methode zugeordnet ist.

Signatur: deleteProperty(target, propertyName) : boolean

Der Rückgabewert ist true, wenn das Property erfolgreich entfernt wurde. Beachten Sie die Invariante, dass eine Eigenschaft nicht gelöscht werden darf, deren PropertyDescriptor configurable:false gesetzt hat, und dass bei einem Objekt, das nicht extensible ist, gar keine Eigenschaften gelöscht werden dürfen.

Die Beispiele, die man im Netz für deleteProperty findet, beziehen sich auf die Implementierung von privaten Eigenschaften und verhindern einen delete von Eigenschaften, deren Name mit einem _ beginnt. Wie das geht, muss nicht mehr extra gezeigt werden.

Interessanter wäre der Einsatz in einem ORM Framework, wo Proxys mit virtuellen Eigenschaften das Mapping übernehmen. Man kann zum Beispiel vorsehen, dass durch den delete einer Eigenschaft, die eine Beziehung repräsentiert, die Beziehung im Modell gelöscht wird.

construct

Für Objekte, die eine interne Construct Methode haben, kann die construct Trap-Methode genutzt werden, um in den new Operator einzugreifen.

Signatur: construct(target, arguments) : Object

construct muss ein Objekt zurückgeben. Die skalaren JavaScript-Typen null, undefined, boolean, number und string führen zu einem TypeError.

Ein construct-Handler übernimmt die Aufgabe, das Zielobjekt zu erzeugen, komplett selbst. Im einfachen Fall delegiert die construct Trap die Objekterzeugung an die übergebene Konstruktorfunktion und tut noch irgendetwas eigenes. Sie könnte einen Logeintrag schreiben, oder das Objekt zentral registrieren.

Beispiel

construct Trap in einem ORMapper

class Irgendwas() {
   ...
}
function RegistriereIrgendwas(i) { ... }

const IrgendProxy = new Proxy(Irgendwas, {
   construct(target, arguments) {
      let i = new target(...arguments);   // Spread-Operator zum Durchreichen der Argumente
      RegistriereIrgendwas(i);
      return i;
   }

Die construct Trap kann aber auch etwas völlig anderes tun.

Beispiel

construct Trap in einem ORMapper

// Ein Ausschnitt aus einem ORM Mapper
class ORMapper {
   static entityHandler = {
      construct(entityInfo, arguments) {
         let objInfo = { entityInfo, data: { id: arguments[0] } };
         let obj = findExistingEntity(objInfo) || new Proxy(objInfo, objectHandler);
         return obj;
      }
   };
  static findExistingEntity(objInfo) {
    ...
  }
  static objectHandler {
    get(objInfo, propertyName) {
      if (propertyName in objInfo.entityInfo) return objInfo.data[propertyName];
      throw new TypeError("Eigenschaft " + propertyName + " existiert in " + 
                          objInfo.entityInfo.name + " nicht");
    }
    set(target, propertyName, newValue) { ... }
    ...
  };
  static defineEntity(name, properties) {
    let entityInfo = { name, properties };
    return new Proxy(entityInfo, entityHandler);
  }   
}

const Person = ORMapper.defineEntity("Person", { name: "string", vorname: "string" });
let person = new Person(7);

Dieses etwas umfangreichere Beispiel zeigt einen möglichen Einsatzort einer construct Trap. Ein ORM System besitzt ein zentrales Steuerungsobjekt ORMapper mit einer Methode defineEntity. Dieser Methode übergibt man einen Namen und eine Deklaration der Eigenschaften, die diese Entität besitzt. Der ORMapper erzeugt einen Proxy, der die Informationen zur Entity als Target bekommt, sowie ein Handlerobjekt für Entitätsproxies. Dieser Proxy wird von der Anwendung gespeichert und dient als Konstruktor für Objekte dieser Entität.

Der entityHandler verwendet nun die construct Trap, um new-Operationen für den Konstruktor zu behandeln. Dazu erzeugt er ein Steuerobjekt objInfo, in dem die Informationen zur Entität abgelegt sind sowie die eigentlichen Objektattribute. Um dieses Steuerobjekt legt er einen anderen Proxy, der einen anderen Handler bekommt, und der es dann übernimmt, die normalen Zugriffe auf ein Objekt auf den Datenspeicher im Steuerobjekt abzubilden. Der ORMapper hat auf diese Weise die Möglichkeit, in jede Interaktion der Anwendung mit dem Entity-Objekt einzugreifen, Änderungen aufzuzeichnen und ggf. zurückzunehmen, das Objekt vielleicht auch an ein UI zu binden.

Natürlich ist dieses Beispiel stark vereinfacht und soll nur das Prinzip zeigen. Ein echter ORMapper muss mehr Möglichkeiten bieten, braucht Fehlerprüfungen und muss vor allem auf Promises setzen, weil DB-Zugriffe normalerweise asynchron ablaufen.

apply

getPrototypeOf

setPrototypeOf

defineProperty

isExtensible

preventExtensions