Jetzt neu: ES2015-Proxys

Addy Osmani
Addy Osmani

ES2015-Proxys (in Chrome 49 und höher) bieten JavaScript eine Intercession API, mit der alle Vorgänge an einem Zielobjekt abgefangen werden können, um die Funktionsweise dieses Ziels zu ändern.

Proxys haben eine Vielzahl von Verwendungsmöglichkeiten, darunter:

  • Interception
  • Objektvirtualisierung
  • Ressourcenverwaltung
  • Profiling oder Logging zur Fehlerbehebung
  • Sicherheits- und Zugriffsverwaltung
  • Verträge zur Verwendung von Objekten

Die Proxy API enthält einen Proxy-Konstruktor, der ein bestimmtes Zielobjekt und ein Handlerobjekt annimmt.

var target = { /* some properties */ };
var handler = { /* trap functions */ };
var proxy = new Proxy(target, handler);

Das Verhalten eines Proxys wird vom Handler gesteuert, der das ursprüngliche Verhalten des Zielobjekts auf verschiedene nützliche Arten ändern kann. Der Handler enthält optionale Trap-Methoden (z. B. .get(), .set(), .apply()), die aufgerufen werden, wenn der entsprechende Vorgang auf dem Proxy ausgeführt wird.

Interception

Beginnen wir mit einem einfachen Objekt und fügen wir ihm mithilfe der Proxy API eine Abfang-Middleware hinzu. Denken Sie daran, dass der erste Parameter, der an den Konstruktor übergeben wird, das Ziel (das Objekt, für das ein Proxy erstellt wird) und der zweite der Handler (der Proxy selbst) ist. Hier können wir Hooks für unsere Getter, Setter oder andere Verhaltensweisen hinzufügen.

var target = {};

var superhero = new Proxy(target, {
    get: function(target, name, receiver) {
        console.log('get was called for:', name);
        return target[name];
    }
});

superhero.power = 'Flight';
console.log(superhero.power);

Wenn wir den obigen Code in Chrome 49 ausführen, erhalten wir Folgendes:

get was called for: power  
"Flight"

Wie wir in der Praxis sehen können, führte die Ausführung unserer Property-Get- oder Property-Set-Methode auf dem Proxyobjekt korrekt zu einem Metaebenenaufruf der entsprechenden Trap auf dem Handler. Zu den Handler-Vorgängen gehören das Lesen von Eigenschaften, die Zuweisung von Eigenschaften und die Funktionsanwendung. Alle werden an die entsprechende Falle weitergeleitet.

Die Trap-Funktion kann einen Vorgang nach Belieben implementieren, z. B. den Vorgang an das Zielobjekt weiterleiten. Das ist standardmäßig der Fall, wenn keine Falle angegeben wird. Hier ist beispielsweise ein No-Op-Weiterleitungsproxy, der genau das tut:

var target = {};

var proxy = new Proxy(target, {});
    // operation forwarded to the target
proxy.paul = 'irish';
// 'irish'. The operation has been  forwarded
console.log(target.paul);

Wir haben uns gerade mit dem Proxying von einfachen Objekten befasst, aber wir können genauso einfach ein Funktionsobjekt proxyen, bei dem eine Funktion unser Ziel ist. Dieses Mal verwenden wir die handler.apply()-Falle:

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

var handler = {
    apply: function(target, thisArg, argumentsList) {
        console.log(`Calculate sum: ${argumentsList}`);
        return target.apply(thisArg, argumentsList);
    }
};

var proxy = new Proxy(sum, handler);
proxy(1, 2);
// Calculate sum: 1, 2
// 3

Proxys identifizieren

Die Identität eines Proxys kann mit den JavaScript-Gleichoperatoren (== und ===) beobachtet werden. Wie wir wissen, vergleichen diese Operatoren bei Anwendung auf zwei Objekte die Objektidentitäten. Das folgende Beispiel veranschaulicht dieses Verhalten. Beim Vergleichen zweier verschiedener Proxys wird „false“ zurückgegeben, obwohl die zugrunde liegenden Ziele identisch sind. Ähnlich unterscheidet sich das Zielobjekt von allen seinen Proxys:

// Continuing previous example

var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false

Idealerweise sollten Sie einen Proxy nicht von einem Nicht-Proxy-Objekt unterscheiden können, damit die Implementierung eines Proxys das Ergebnis Ihrer App nicht wirklich beeinflusst. Dies ist einer der Gründe, warum die Proxy API keine Möglichkeit bietet, zu prüfen, ob ein Objekt ein Proxy ist, und auch keine Fallen für alle Vorgänge an Objekten bereitstellt.

Anwendungsfälle

Wie bereits erwähnt, haben Proxys eine Vielzahl von Anwendungsfällen. Viele der oben genannten Funktionen, z. B. Zugriffssteuerung und Profiling, fallen unter generische Wrapper: Proxys, die andere Objekte in denselben Adressbereich einschließen. Auch Virtualisierung wurde erwähnt. Virtuelle Objekte sind Proxys, die andere Objekte emulieren, ohne dass sich diese Objekte im selben Adressraum befinden müssen. Beispiele sind Remote-Objekte (die Objekte in anderen Bereichen emulieren) und transparente Futures (die Ergebnisse emulieren, die noch nicht berechnet wurden).

Proxys als Handler

Ein ziemlich häufiger Anwendungsfall für Proxy-Handler besteht darin, Validierungs- oder Zugriffssteuerungsüberprüfungen durchzuführen, bevor ein Vorgang auf einem verpackten Objekt ausgeführt wird. Nur wenn die Prüfung erfolgreich ist, wird der Vorgang weitergeleitet. Das folgende Beispiel für die Validierung veranschaulicht dies:

var validator = {
    set: function(obj, prop, value) {
    if (prop === 'yearOfBirth') {
        if (!Number.isInteger(value)) {
        throw new TypeError('The yearOfBirth is not an integer');
        }

        if (value > 3000) {
        throw new RangeError('The yearOfBirth seems invalid');
        }
    }

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

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

person.yearOfBirth = 1986;
console.log(person.yearOfBirth); // 1986
person.yearOfBirth = 'eighties'; // Throws an exception
person.yearOfBirth = 3030; // Throws an exception

Komplexere Beispiele für dieses Muster können alle verschiedenen Vorgänge berücksichtigen, die Proxy-Handler abfangen können. Man kann sich vorstellen, dass bei einer Implementierung das Muster der Zugriffsüberprüfung dupliziert und der Vorgang in jeder Falle weitergeleitet werden muss.

Das kann schwierig sein, da jede Operation möglicherweise unterschiedlich weitergeleitet werden muss. Im Idealfall könnten alle Vorgänge einheitlich über eine einzige Falle geleitet werden. In diesem Fall müsste der Handler die Validierungsüberprüfung nur einmal in der einzelnen Falle ausführen. Dazu können Sie den Proxy-Handler selbst als Proxy implementieren. Dies würde den Rahmen dieses Artikels sprengen.

Objekterweiterung

Ein weiterer häufiger Anwendungsfall für Proxys ist die Erweiterung oder Neudefinition der Semantik von Vorgängen auf Objekten. Sie können beispielsweise festlegen, dass ein Handler Vorgänge protokolliert, Beobachter benachrichtigt, Ausnahmen auslöst, anstatt „undefiniert“ zurückzugeben, oder Vorgänge an verschiedene Ziele für die Speicherung weiterleitet. In diesen Fällen kann die Verwendung eines Proxys zu einem ganz anderen Ergebnis führen als die Verwendung des Zielobjekts.

function extend(sup,base) {

    var descriptor = Object.getOwnPropertyDescriptor(base.prototype,"constructor");

    base.prototype = Object.create(sup.prototype);

    var handler = {
    construct: function(target, args) {
        var obj = Object.create(base.prototype);
        this.apply(target,obj, args);
        return obj;
    },

    apply: function(target, that, args) {
        sup.apply(that,args);
        base.apply(that,args);
    }
    };

    var proxy = new Proxy(base, handler);
    descriptor.value = proxy;
    Object.defineProperty(base.prototype, "constructor", descriptor);
    return proxy;
}

var Vehicle = function(name){
    this.name = name;
};

var Car = extend(Vehicle, function(name, year) {
    this.year = year;
});

Car.prototype.style = "Saloon";

var Tesla = new Car("Model S", 2016);

console.log(Tesla.style); // "Saloon"
console.log(Tesla.name); // "Model S"
console.log(Tesla.year);  // 2016

Zugriffssteuerung

Ein weiterer Anwendungsfall für Proxys ist die Zugriffssteuerung. Anstatt ein Zielobjekt an nicht vertrauenswürdigen Code weiterzuleiten, könnte man seinen Proxy in einer Art Schutzhülle übergeben. Sobald die App feststellt, dass der nicht vertrauenswürdige Code eine bestimmte Aufgabe abgeschlossen hat, kann sie die Referenz widerrufen, wodurch der Proxy von seinem Ziel getrennt wird. Die Membran würde diese Trennung rekursiv auf alle Objekte ausweiten, die über das ursprünglich definierte Ziel erreichbar sind.

Reflexion mit Proxys verwenden

Reflect ist ein neues integriertes Objekt, das Methoden für abfangsabhänige JavaScript-Vorgänge bietet. Das ist sehr nützlich für die Arbeit mit Proxys. Reflect-Methoden sind in der Tat mit denen von Proxy-Handlern identisch.

Statisch typisierte Sprachen wie Python oder C# bieten schon lange eine Reflection API, aber JavaScript benötigte keine, da es sich um eine dynamische Sprache handelt. Man könnte argumentieren, dass ES5 bereits einige Reflexionsfunktionen hat, z. B. Array.isArray() oder Object.getOwnPropertyDescriptor(), die in anderen Sprachen als Reflexion betrachtet würden. ES2015 führt eine Reflection API ein, in der zukünftige Methoden für diese Kategorie enthalten sein werden. Dadurch lassen sich diese Methoden leichter nachvollziehen. Das ist sinnvoll, da „Object“ als Basisprototyp und nicht als Sammelbecken für Reflexionsmethoden gedacht ist.

Mit Reflect können wir unser vorheriges Superhero-Beispiel für die korrekte Feldabfangung bei unseren Get- und Set-Traps so verbessern:

// Field interception with Proxy and the Reflect API

var pioneer = new Proxy({}, {
    get: function(target, name, receiver) {
        console.log(`get called for field: ${name}`);
        return Reflect.get(target, name, receiver);
    },

    set: function(target, name, value, receiver) {
        console.log(`set called for field: ${name} and value: ${value}`);
        return Reflect.set(target, name, value, receiver);
    }
});

pioneer.firstName = 'Grace';
pioneer.secondName = 'Hopper';
// Grace
pioneer.firstName

Dies gibt Folgendes zurück:

set called for field: firstName and value: Grace
set called for field: secondName and value: Hopper
get called for field: firstName

Ein weiteres Beispiel:

  • Umschließen Sie eine Proxydefinition in einem benutzerdefinierten Konstruktor, damit nicht jedes Mal, wenn Sie mit einer bestimmten Logik arbeiten möchten, manuell ein neuer Proxy erstellt werden muss.

  • Fügen Sie die Möglichkeit hinzu, Änderungen zu speichern, aber nur, wenn Daten tatsächlich geändert wurden (hypothetisch, weil der Speichervorgang sehr teuer ist).

function Customer() {

    var proxy = new Proxy({
    save: function(){
        if (!this.dirty){
        return console.log('Not saving, object still clean');
        }
        console.log('Trying an expensive saving operation: ', this.changedProperties);
    },

    }, {

    set: function(target, name, value, receiver) {
        target.dirty = true;
        target.changedProperties = target.changedProperties || [];

        if(target.changedProperties.indexOf(name) == -1){
        target.changedProperties.push(name);
        }
        return Reflect.set(target, name, value, receiver);
    }

    });

    return proxy;
}


var customer = new Customer();

customer.name = 'seth';
customer.surname = 'thompson';
// Trying an expensive saving operation:  ["name", "surname"]
customer.save();

Weitere Reflect API-Beispiele finden Sie in ES6 Proxies von Tagtree.

Polyfilling Object.observe()

Object.observe() wird zwar eingestellt, aber es ist jetzt möglich, sie mit ES2015-Proxys zu polyfillen. Simon Blackwell hat vor Kurzem einen Proxy-basierten Object.observe()-Shim geschrieben, den Sie sich ansehen sollten. Erik Arvidsson hat bereits 2012 eine ziemlich vollständige Spezifikation geschrieben.

Unterstützte Browser

ES2015-Proxys werden in Chrome 49, Opera, Microsoft Edge und Firefox unterstützt. Die öffentliche Meinung zu der Funktion in Safari ist gemischt, aber wir bleiben optimistisch. Reflect ist in Chrome, Opera und Firefox verfügbar und wird derzeit für Microsoft Edge entwickelt.

Google hat eine eingeschränkte Polyfill für Proxy veröffentlicht. Diese Funktion kann nur für generische Wrapper verwendet werden, da nur Proxy-Properties verwendet werden können, die zum Zeitpunkt der Proxy-Erstellung bekannt waren.

Weitere Informationen