Introduzione ai proxy ES2015

Addy Osmani
Addy Osmani

I proxy ES2015 (in Chrome 49 e versioni successive) forniscono a JavaScript un'API di intercessione, che ci consente di intercettare o intercettare tutte le operazioni su un oggetto target e modificare il funzionamento di questo target.

I proxy hanno un gran numero di utilizzi, tra cui:

  • Intercetto
  • Virtualizzazione degli oggetti
  • Gestione delle risorse
  • Profilazione o logging per il debug
  • Sicurezza e controllo dell'accesso
  • Contratti per l'utilizzo di oggetti

L'API Proxy contiene un costruttore Proxy che accetta un oggetto target designato e un oggetto gestore.

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

Il comportamento di un proxy è controllato dall'handler, che può modificare il comportamento originale dell'oggetto target in diversi modi utili. Il gestore contiene metodi di trappola facoltativi (ad es..get(), .set(), .apply()) chiamati quando viene eseguita l'operazione corrispondente sul proxy.

Intercetto

Iniziamo prendendo un oggetto normale e aggiungendo un middleware di intercettazione utilizzando l'API Proxy. Ricorda che il primo parametro passato al costruttore è il target (l'oggetto di cui viene eseguito il proxy) e il secondo è il gestore (il proxy stesso). Qui possiamo aggiungere hook per i nostri getter, setter o altri comportamenti.

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

Se esegui il codice riportato sopra in Chrome 49, ottieni quanto segue:

get was called for: power  
"Flight"

Come possiamo vedere nella pratica, l'esecuzione corretta di get o set della proprietà sull'oggetto proxy ha generato una chiamata a livello di meta alla trap corrispondente sul gestore. Le operazioni degli handler includono letture delle proprietà, assegnazione delle proprietà e applicazione di funzioni, che vengono tutte inoltrate alla trap corrispondente.

La funzione di trappola può, se lo desidera, implementare un'operazione in modo arbitrario (ad es.inoltrare l'operazione all'oggetto target). Questo è ciò che accade per impostazione predefinita se non viene specificata una trappola. Ad esempio, ecco un proxy di inoltro senza operazioni che fa proprio questo:

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

Abbiamo appena esaminato il proxy di oggetti semplici, ma possiamo fare altrettanto facilmente il proxy di un oggetto funzione, se la funzione è il nostro target. Questa volta utilizzeremo la trappola 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

Identificazione dei proxy

L'identità di un proxy può essere osservata utilizzando gli operatori di uguaglianza di JavaScript (== e ===). Come sappiamo, se applicati a due oggetti, questi operatori confrontano le identità degli oggetti. L'esempio seguente mostra questo comportamento. Il confronto di due proxy distinti restituisce false nonostante i target sottostanti siano gli stessi. Analogamente, l'oggetto target è diverso da qualsiasi proxy:

// Continuing previous example

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

Idealmente, non dovresti essere in grado di distinguere un proxy da un oggetto non proxy, in modo che l'implementazione di un proxy non influisca sul risultato della tua app. Questo è uno dei motivi per cui l'API Proxy non include un modo per verificare se un oggetto è un proxy né fornisce trappole per tutte le operazioni sugli oggetti.

Casi d'uso

Come accennato, i proxy hanno una vasta gamma di casi d'uso. Molti di quelli sopra indicati, come il controllo dell'accesso e il profiling, rientrano nei wrapper generici: proxy che avvolgono altri oggetti nello stesso "spazio" di indirizzi. È stata menzionata anche la virtualizzazione. Gli oggetti virtuali sono proxy che simulano altri oggetti senza che questi debbano trovarsi nello stesso spazio degli indirizzi. Alcuni esempi sono oggetti remoti (che simulano oggetti in altri spazi) e futures trasparenti (che simulano risultati non ancora calcolati).

Proxy come gestori

Un caso d'uso abbastanza comune per i gestori di proxy è eseguire controlli di convalida o controllo dell'accesso prima di eseguire un'operazione su un oggetto con wrapping. L'operazione viene inoltrata solo se il controllo va a buon fine. L'esempio di convalida riportato di seguito lo dimostra:

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

Esempi più complessi di questo pattern potrebbero prendere in considerazione tutte le diverse operazioni che gli handler proxy possono intercettare. Si potrebbe immaginare un'implementazione che debba duplicare il pattern di controllo dell'accesso e inoltrare l'operazione in ogni trappola.

Può essere complicato estrarne facilmente l'astrazione, dato che ogni operazione potrebbe dover essere inoltrata in modo diverso. In uno scenario ideale, se tutte le operazioni potessero essere incanalate in modo uniforme in una sola trappola, il gestore dovrebbe eseguire il controllo di convalida una sola volta nella singola trappola. Puoi farlo implementando il gestore del proxy stesso come proxy. Purtroppo, questo argomento non rientra nell'ambito di questo articolo.

Estensione di oggetti

Un altro caso d'uso comune per i proxy è estendere o ridefinire la semantica delle operazioni sugli oggetti. Ad esempio, potresti volere che un gestore registri le operazioni, invii notifiche agli osservatori, lanci eccezioni anziché restituire undefined o reindirizzi le operazioni a target di archiviazione diversi. In questi casi, l'utilizzo di un proxy potrebbe portare a un risultato molto diverso rispetto all'utilizzo dell'oggetto target.

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

Controllo dell'accesso

Il controllo dell'accesso è un altro buon caso d'uso per i proxy. Anziché passare un oggetto target a un codice non attendibile, è possibile passare il relativo proxy avvolto in una sorta di membrana protettiva. Quando l'app ritiene che il codice non attendibile abbia completato una determinata attività, può revocare il riferimento che scollega il proxy dal suo target. La membrana estenderà questo distacco in modo ricorsivo a tutti gli oggetti raggiungibili dal target originale definito.

Utilizzo della riflessione con i proxy

Reflect è un nuovo oggetto integrato che fornisce metodi per le operazioni JavaScript intercettabili, molto utili per lavorare con i proxy. Infatti, i metodi Reflect sono gli stessi degli handler proxy.

I linguaggi con tipi statici come Python o C# offrono da tempo un'API di riflessione, ma JavaScript non ne ha mai avuto bisogno perché è un linguaggio dinamico. Si può sostenere che ES5 abbia già diverse funzionalità di riflessione, come Array.isArray() o Object.getOwnPropertyDescriptor(), che verrebbero considerate riflessioni in altre lingue. ES2015 introduce un'API di riflessione che ospiterà i metodi futuri per questa categoria, semplificandone la comprensione. Questo ha senso perché Object è pensato come un prototipo di base anziché un bucket per i metodi di riflessione.

Utilizzando Reflect, possiamo migliorare l'esempio di Supereroe precedente per l'intercettazione corretta dei campi nelle trappole get e set come segue:

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

Quali output:

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

Un altro esempio è quando si vuole:

  • Inserisci una definizione di proxy all'interno di un costruttore personalizzato per evitare di creare manualmente un nuovo proxy ogni volta che vogliamo utilizzare una logica specifica.

  • Aggiungi la possibilità di "salvare" le modifiche, ma solo se i dati sono stati effettivamente modificati (ipoteticamente a causa del costo elevato dell'operazione di salvataggio).

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

Per altri esempi dell'API Reflect, consulta ES6 Proxies di Tagtree.

Polyfilling Object.observe()

Anche se diciamo addio a Object.observe(), ora è possibile eseguirne il polyfill utilizzando i proxy ES2015. Di recente Simon Blackwell ha scritto un shim Object.observe() basato su proxy che vale la pena provare. Nel 2012, Erik Arvidsson ha anche scritto una versione abbastanza completa delle specifiche.

Supporto browser

I proxy ES2015 sono supportati in Chrome 49, Opera, Microsoft Edge e Firefox. Safari ha ricevuto indicatori pubblici contrastanti sulla funzionalità, ma rimaniamo ottimisti. Reflect è disponibile su Chrome, Opera e Firefox ed è in fase di sviluppo per Microsoft Edge.

Google ha rilasciato un polyfill limitato per Proxy. Questo valore può essere utilizzato solo per i wrapper generici, in quanto può eseguire il proxy solo delle proprietà note al momento della creazione di un proxy.

Per approfondire