Jetzt neu: ES2015-Proxys

Osmani
Addy Osmani

ES2015-Proxys (in Chrome 49 und höher) stellen JavaScript mit einer Intercession API bereit, mit der wir alle Vorgänge auf einem Zielobjekt erfassen oder abfangen und die Funktionsweise dieses Ziels ändern können.

Für Proxys gibt es viele Einsatzmöglichkeiten, z. B.:

  • Interception
  • Objektvirtualisierung
  • Ressourcenverwaltung
  • Profilerstellung oder Logging für die Fehlerbehebung
  • Sicherheits- und Zugriffsverwaltung
  • Verträge zur Objektnutzung

Die Proxy-API enthält einen Proxy-Konstruktor, der ein festgelegtes Zielobjekt und ein Handler-Objekt verwendet.

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 target-Objekts auf verschiedene 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 ihm mithilfe der Proxy API eine Interception-Middleware hinzu. Denken Sie daran, dass der erste Parameter, der an den Konstruktor übergeben wird, das Ziel (das Objekt, das weitergeleitet 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 korrekte Ausführung des Attributs get oder der Eigenschaftsfestlegung für das Proxyobjekt zu einem Meta-Level-Aufruf an den entsprechenden Trap im Handler. Zu Handler-Vorgängen gehören Lesevorgänge, die Property-Zuweisung und die Funktionsanwendung, die alle an den entsprechenden Trap weitergeleitet werden.

Die Trap-Funktion kann, falls gewünscht, einen Vorgang beliebig implementieren (z. B. Weiterleitung des Vorgangs an das Zielobjekt). Das passiert tatsächlich, wenn keine Falle spezifiziert wird. Hier ist z.B. ein managementfreier Weiterleitungs-Proxy, der genau dies 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 das Proxying von einfachen Objekten angesehen, aber wir können genauso einfach ein Funktionsobjekt im Proxy verwenden, bei dem eine Funktion unser Ziel ist. Dieses Mal verwenden wir den Trap handler.apply():

// 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 mithilfe der JavaScript-Gleichheitsoperatoren (== und ===) beobachtet werden. Wie wir wissen, werden bei Anwendung dieser Operatoren Objektidentitäten verglichen. Das nächste Beispiel veranschaulicht dieses Verhalten. Beim Vergleich von zwei verschiedenen Proxys wird „false“ zurückgegeben, obwohl die zugrunde liegenden Ziele identisch sind. Außerdem unterscheidet sich das Zielobjekt von 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 das Einrichten eines Proxys das Ergebnis Ihrer App nicht wirklich beeinträchtigt. 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 Traps für alle Vorgänge für Objekte bereitstellt.

Anwendungsfälle

Wie bereits erwähnt, gibt es für Proxys eine Vielzahl von Anwendungsfällen. Viele der obigen Elemente, z. B. Zugriffssteuerung und Profilerstellung, fallen unter Generische Wrapper: Proxys, die andere Objekte im selben „Bereich“ für Adresse verpacken. 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 hierfür sind Remote-Objekte, die Objekte in anderen Gruppenbereichen emulieren, und transparente Future-Objekte (Emulation von noch nicht berechneten Ergebnissen).

Proxys als Handler

Proxy-Handler werden häufig zum Ausführen einer Validierung oder Zugriffssteuerung verwendet, bevor ein Vorgang an einem umschlossenen Objekt durchgeführt wird. Nur wenn die Prüfung erfolgreich ist, wird der Vorgang weitergeleitet. Das folgende Validierungsbeispiel 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

Bei komplexeren Beispielen für dieses Muster könnten alle verschiedenen Vorgangs-Proxy-Handler berücksichtigt werden, die sie abfangen können. Man könnte sich vorstellen, dass eine Implementierung das Muster der Zugriffsprüfung und der Weiterleitung des Vorgangs in jedem Fall duplizieren müsste.

Dies kann schwierig zu abstrahieren sein, da jeder Vorgang anders weitergeleitet werden muss. Wenn in einem perfekten Szenario alle Vorgänge einheitlich durch nur einen Trap geleitet werden könnten, müsste der Handler die Validierungsprüfung nur einmal in diesem einzelnen Trap durchführen. Sie könnten dies tun, indem Sie den Proxy-Handler selbst als Proxy implementieren. Dies wird in diesem Artikel leider nicht behandelt.

Objekterweiterung

Ein weiterer häufiger Anwendungsfall für Proxys ist das Erweitern oder Neudefinieren der Semantik von Vorgängen an Objekten. Ein Handler kann beispielsweise Vorgänge protokollieren, Beobachter benachrichtigen, Ausnahmen auslösen, anstatt nicht definierte zurückzugeben, oder Vorgänge zum Speichern an andere Ziele weiterleiten. 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

Die Zugriffssteuerung ist ein weiterer guter Anwendungsfall für Proxys. Anstatt ein Zielobjekt an einen nicht vertrauenswürdigen Code zu übergeben, könnte man seinen Proxy übergeben, der von einer Art Schutzmembran umgeben ist. Sobald die App feststellt, dass der nicht vertrauenswürdige Code eine bestimmte Aufgabe abgeschlossen hat, kann die Referenz widerrufen werden. Dadurch wird der Proxy von seinem Ziel getrennt. Die Membran würde diese Trennung rekursiv auf alle Objekte ausdehnen, die vom ursprünglich definierten Ziel aus erreichbar sind.

Reflexion mit Proxys verwenden

Reflect ist ein neues integriertes Objekt, das Methoden für abfangende JavaScript-Vorgänge bereitstellt, was für die Arbeit mit Proxys sehr nützlich ist. Die Reflect-Methoden entsprechen denen von Proxy-Handlern.

Statisch typisierte Sprachen wie Python oder C# bieten seit Langem eine Reflexions-API an, für JavaScript war jedoch keine dynamische Sprache notwendig. Man könnte behaupten, dass ES5 bereits einige Reflexionsmerkmale wie Array.isArray() oder Object.getOwnPropertyDescriptor() hat, die als Reflexion in anderen Sprachen gelten würden. In ES2015 wird eine Reflection API eingeführt, in der zukünftige Methoden für diese Kategorie gespeichert werden, sodass sie leichter verständlich sind. Dies ergibt Sinn, da Objekt ein Basisprototyp und kein Bucket für Reflexionsmethoden sein soll.

Mit Reflect können wir unser früheres Superhelden-Beispiel für das korrekte Abfangen von Feldern in unseren Get- und Set-Falls wie folgt 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

Welche Ausgaben:

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

Ein weiteres Beispiel:

  • Schließen Sie eine Proxy-Definition in einen benutzerdefinierten Konstruktor ein, damit nicht jedes Mal ein neuer Proxy erstellt werden muss, wenn wir mit einer bestimmten Logik arbeiten möchten.

  • Fügen Sie die Möglichkeit zum „Speichern“ von Änderungen hinzu, aber nur, wenn Daten tatsächlich geändert wurden (hypothetisch da 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 unter ES6-Proxys von Tagtree.

Polyfilling-Objekt.observe()

Object.observe() wurde zwar verabschiedet, aber es ist jetzt möglich, sie mithilfe von ES2015-Proxys zu Polyfill zu machen. Simon Blackwell hat kürzlich einen proxybasierten Object.observe()-Shim geschrieben, den Sie sich ansehen sollten. Auch Erik Arvidsson hat 2012 eine ziemlich vollständige Version der Spezifikation verfasst.

Unterstützte Browser

ES2015-Proxys werden in Chrome 49, Opera, Microsoft Edge und Firefox unterstützt. Safari hat gemischte öffentliche Signale in Bezug auf die Funktion, aber wir sind weiterhin optimistisch. „Reflect“ ist in Chrome, Opera und Firefox integriert und befindet sich noch in der Entwicklung für Microsoft Edge.

Google hat ein eingeschränktes Polyfill für Proxy veröffentlicht. Dieser kann nur für allgemeine Wrapper verwendet werden, da er nur Proxyattribute unterstützt, die zum Zeitpunkt der Proxyerstellung bekannt sind.

Weitere Informationen