Introductie van ES2015-proxy's

Addy Osmani
Addy Osmani

ES2015-proxy's (in Chrome 49 en hoger) voorzien JavaScript van een tussenkomst-API, waardoor we alle bewerkingen op een doelobject kunnen onderscheppen of onderscheppen en de manier waarop dit doel werkt kunnen wijzigen.

Proxy's hebben een groot aantal toepassingen, waaronder:

  • Onderschepping
  • Objectvirtualisatie
  • Beheer van hulpbronnen
  • Profilering of logboekregistratie voor foutopsporing
  • Beveiliging en toegangscontrole
  • Contracten voor objectgebruik

De Proxy-API bevat een Proxy-constructor die een aangewezen doelobject en een handler-object gebruikt.

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

Het gedrag van een proxy wordt bestuurd door de handler , die het oorspronkelijke gedrag van het doelobject op een flink aantal nuttige manieren kan wijzigen. De handler bevat optionele trap-methoden (bijv .get() , .set() , .apply() ) die worden aangeroepen wanneer de overeenkomstige bewerking op de proxy wordt uitgevoerd.

Onderschepping

Laten we beginnen door een gewoon object te nemen en er wat interceptie-middleware aan toe te voegen met behulp van de Proxy API. Houd er rekening mee dat de eerste parameter die aan de constructor wordt doorgegeven het doel is (het object dat wordt geproxyd) en de tweede de handler (de proxy zelf). Dit is waar we haken kunnen toevoegen voor onze getters, setters of ander gedrag.

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);

Als we de bovenstaande code uitvoeren in Chrome 49, krijgen we het volgende:

get was called for: power  
"Flight"

Zoals we in de praktijk kunnen zien, resulteerde het correct uitvoeren van onze eigenschap get of eigenschap ingesteld op het proxy-object in een aanroep op metaniveau naar de overeenkomstige trap op de handler. Handlerbewerkingen omvatten het lezen van eigenschappen, het toewijzen van eigenschappen en het toepassen van functies, die allemaal worden doorgestuurd naar de overeenkomstige trap.

De trap-functie kan, indien gewenst, een bewerking willekeurig implementeren (bijvoorbeeld de bewerking doorsturen naar het doelobject). Dit is inderdaad wat er standaard gebeurt als er geen trap wordt gespecificeerd. Hier is bijvoorbeeld een no-op forwarding proxy die precies dit doet:

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);

We hebben alleen gekeken naar het proxyen van gewone objecten, maar we kunnen net zo gemakkelijk een functieobject proxyen, waarbij een functie ons doel is. Deze keer gebruiken we de handler.apply() trap:

// 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

Proxy's identificeren

De identiteit van een proxy kan worden waargenomen met behulp van de JavaScript-gelijkheidsoperatoren ( == en === ). Zoals we weten, vergelijken deze operatoren, wanneer ze op twee objecten worden toegepast, objectidentiteiten. Het volgende voorbeeld demonstreert dit gedrag. Het vergelijken van twee verschillende proxy's levert 'false' op, ondanks dat de onderliggende doelen hetzelfde zijn. Op dezelfde manier verschilt het doelobject van al zijn proxy's:

// Continuing previous example

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

Idealiter zou u een proxy niet moeten kunnen onderscheiden van een niet-proxyobject, zodat het instellen van een proxy niet echt van invloed is op de uitkomst van uw app. Dit is één van de redenen dat de Proxy API geen manier bevat om te controleren of een object een proxy is, en ook geen traps biedt voor alle bewerkingen op objecten.

Gebruiksgevallen

Zoals gezegd hebben proxy's een breed scala aan gebruiksscenario's. Veel van de bovenstaande zaken, zoals toegangscontrole en profilering, vallen onder Generieke wrappers : proxy's die andere objecten in dezelfde adresruimte verpakken. Virtualisatie werd ook genoemd. Virtuele objecten zijn proxy's die andere objecten emuleren zonder dat deze objecten zich in dezelfde adresruimte hoeven te bevinden. Voorbeelden hiervan zijn onder meer objecten op afstand (die objecten in andere ruimtes emuleren) en transparante toekomsten (die resultaten emuleren die nog niet zijn berekend).

Proxy's als handlers

Een vrij algemeen gebruik van proxy-handlers is het uitvoeren van validatie- of toegangscontrolecontroles voordat een bewerking op een ingepakt object wordt uitgevoerd. Alleen als de controle succesvol is, wordt de bewerking doorgestuurd. Het onderstaande validatievoorbeeld laat dit zien:

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

Complexere voorbeelden van dit patroon kunnen rekening houden met alle verschillende bewerkingen die proxy-handlers kunnen onderscheppen. Je zou je kunnen voorstellen dat een implementatie het patroon van toegangscontrole en het doorsturen van de bewerking in elke trap moet dupliceren.

Dit kan lastig zijn om gemakkelijk te begrijpen, aangezien elke opdracht mogelijk anders moet worden doorgestuurd. In een perfect scenario, als alle handelingen op uniforme wijze door slechts één valstrik zouden kunnen worden geleid, zou de afhandelaar de validatiecontrole slechts één keer in de enkele valstrik hoeven uit te voeren. U kunt dit doen door de proxy-handler zelf als proxy te implementeren. Dit valt helaas buiten het bestek van dit artikel.

Objectextensie

Een ander veelvoorkomend gebruiksscenario voor proxy's is het uitbreiden of opnieuw definiëren van de semantiek van bewerkingen op objecten. U wilt bijvoorbeeld dat een handler bewerkingen registreert, waarnemers op de hoogte stelt, uitzonderingen genereert in plaats van ongedefinieerd terug te sturen, of bewerkingen omleidt naar andere doelen voor opslag. In deze gevallen kan het gebruik van een proxy tot een heel ander resultaat leiden dan het gebruik van het doelobject.

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

Toegangscontrole

Toegangscontrole is een ander goed gebruiksscenario voor proxy's. In plaats van een doelobject door te geven aan een stukje niet-vertrouwde code, zou je de proxy ervan kunnen doorgeven, gewikkeld in een soort beschermend membraan. Zodra de app oordeelt dat de niet-vertrouwde code een bepaalde taak heeft voltooid, kan deze de referentie intrekken die de proxy loskoppelt van zijn doel. Het membraan zou deze onthechting recursief uitbreiden naar alle objecten die bereikbaar zijn vanaf het oorspronkelijke doel dat was gedefinieerd.

Reflectie gebruiken met proxy's

Reflect is een nieuw ingebouwd object dat methoden biedt voor onderschepbare JavaScript-bewerkingen, erg handig voor het werken met proxy's. In feite zijn Reflect-methoden dezelfde als die van proxy-handlers .

Statisch getypeerde talen zoals Python of C# bieden al lang een reflectie-API, maar JavaScript had niet echt een dynamische taal nodig. Je kunt stellen dat ES5 al een flink aantal reflectiefuncties heeft, zoals Array.isArray() of Object.getOwnPropertyDescriptor() die in andere talen als reflectie zouden worden beschouwd. ES2015 introduceert een Reflection API die toekomstige methoden voor deze categorie zal huisvesten, waardoor ze gemakkelijker te redeneren zijn. Dit is logisch omdat Object bedoeld is als een basisprototype en niet als een emmer voor reflectiemethoden.

Met behulp van Reflect kunnen we ons eerdere superheldenvoorbeeld als volgt verbeteren voor een goede veldonderschepping bij onze get- en set-traps:

// 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

Welke uitgangen:

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

Een ander voorbeeld is waar men het volgende zou willen doen:

  • Plaats een proxydefinitie in een aangepaste constructor om te voorkomen dat u elke keer dat u met specifieke logica wilt werken, handmatig een nieuwe proxy moet maken.

  • Voeg de mogelijkheid toe om wijzigingen 'op te slaan', maar alleen als gegevens daadwerkelijk zijn gewijzigd (hypothetisch omdat de opslagbewerking erg duur is).

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();

Zie ES6 Proxies van Tagtree voor meer Reflect API-voorbeelden.

Polyfilling Object.observe()

Hoewel we afscheid nemen van Object.observe() , is het nu mogelijk om ze te polyfillen met behulp van ES2015 Proxies. Simon Blackwell heeft onlangs een op proxy gebaseerde Object.observe() -shim geschreven die de moeite van het bekijken waard is. Erik Arvidsson schreef in 2012 ook een redelijk complete versie.

Browser-ondersteuning

ES2015-proxy's worden ondersteund in Chrome 49, Opera, Microsoft Edge en Firefox. Safari heeft gemengde publieke signalen gekregen over de functie, maar we blijven optimistisch. Reflect is beschikbaar in Chrome, Opera en Firefox en is in ontwikkeling voor Microsoft Edge.

Google heeft een beperkte polyfill voor Proxy uitgebracht. Dit kan alleen worden gebruikt voor generieke wrappers , omdat het alleen proxy-eigenschappen kan weergeven die bekend zijn op het moment dat een proxy wordt gemaakt.

Verder lezen