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 eseguire il trap o intercettare tutte le operazioni su un oggetto di destinazione e modificare il funzionamento di questo target.

I proxy hanno molti 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 di destinazione 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 dal handler, che può modificare il comportamento originale dell'oggetto target in diversi modi utili. Il gestore contiene metodi di trap facoltativi (ad es..get(), .set(), .apply()) chiamati quando viene eseguita l'operazione corrispondente sul proxy.

Intercetto

Iniziamo prendendo un oggetto semplice e aggiungendovi un middleware di intercettazione utilizzando l'API Proxy. Ricorda che il primo parametro passato al costruttore è il target (l'oggetto sottoposto a 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);

Eseguendo il codice riportato sopra in Chrome 49 otteniamo quanto segue:

get was called for: power  
"Flight"

Come possiamo vedere in pratica, l'esecuzione della nostra proprietà get o property impostata sull'oggetto proxy ha generato correttamente una chiamata a livello di meta al trap corrispondente sul gestore. Le operazioni del gestore includono letture delle proprietà, l'assegnazione delle proprietà e l'applicazione della funzione, tutte inoltrate al trap corrispondente.

La funzione trap può, se lo desidera, implementare un'operazione in modo arbitrario (ad esempio inoltrando l'operazione all'oggetto di destinazione). Questo è di fatto ciò che accade per impostazione predefinita se un trap non viene specificato. Ad esempio, ecco un proxy di inoltro autonomo 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 per oggetti semplici, ma possiamo altrettanto facilmente eseguire il proxy per un oggetto funzione, in cui una funzione è il nostro target. Questa volta useremo 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 JavaScript (== e ===). Come è noto, questi operatori confrontano le identità degli oggetti quando sono applicati a due oggetti. Il prossimo esempio mostra questo comportamento. Il confronto di due proxy distinti restituisce false, nonostante i target sottostanti siano gli stessi. Analogamente, l'oggetto di destinazione è diverso da qualsiasi suo 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 la configurazione di un proxy non influisca davvero sul risultato dell'app. Questo è uno dei motivi per cui l'API proxy non include un modo per verificare se un oggetto è un proxy né fornisce trap per tutte le operazioni sugli oggetti.

Casi d'uso

Come accennato, i proxy hanno una vasta gamma di casi d'uso. Molti di questi, ad esempio il controllo dell'accesso e la profilazione, rientrano nei wrapper generici: proxy che aggregano altri oggetti nello stesso "spazio di indirizzo". È stata menzionata anche la virtualizzazione. Gli oggetti virtuali sono proxy che emulano altri oggetti senza che questi debbano trovarsi nello stesso spazio degli indirizzi. Alcuni esempi includono oggetti remoti (che emulano oggetti in altri spazi) e future trasparenti (emulazione di risultati non ancora calcolati).

proxy come gestori

Un caso d'uso piuttosto comune per i gestori proxy è l'esecuzione di controlli di convalida o di controllo dell'accesso prima di eseguire un'operazione su un oggetto con wrapping. L'operazione viene inoltrata solo se la verifica ha esito positivo. Il seguente esempio di convalida 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 tutti i diversi gestori proxy delle operazioni che possono intercettare. Si potrebbe immaginare un'implementazione che debba duplicare il modello di controllo dell'accesso e inoltro dell'operazione in ogni trap.

Questo può essere difficile da astrarre, poiché ogni operazione potrebbe dover essere inoltrata in modo diverso. In uno scenario perfetto, se tutte le operazioni possano essere incanalate in modo uniforme attraverso un'unica trappola, il gestore dovrebbe eseguire il controllo di convalida solo una volta nella singola trappola. A tale scopo, implementa il gestore proxy stesso come proxy. Purtroppo ciò non rientra nell'ambito di questo articolo.

Estensione oggetto

Un altro caso d'uso comune per i proxy è l'estensione o la ridefinizione della semantica delle operazioni sugli oggetti. Ad esempio, potresti volere che un gestore registri le operazioni, invii una notifica agli osservatori, generi eccezioni invece di restituire non definite o reindirizzi le operazioni a destinazioni diverse per l'archiviazione. In questi casi, l'utilizzo di un proxy potrebbe portare a un risultato molto diverso rispetto all'uso 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. Invece di passare un oggetto target a una porzione di codice non attendibile, si potrebbe passare il suo proxy avvolto in una sorta di membrana protettiva. Quando l'app ritiene che il codice non attendibile ha completato una determinata attività, può revocare il riferimento che scollega il proxy dalla destinazione. La membrana estenderebbe questo distacco in modo ricorsivo a tutti gli oggetti raggiungibili dal bersaglio originale definito.

Utilizzo della riflessione con i proxy

Reflect è un nuovo oggetto integrato che fornisce metodi per operazioni JavaScript intercettabili, molto utile per lavorare con proxy. Infatti, i metodi Reflect sono gli stessi dei gestori proxy.

I linguaggi digitati in modo statico come Python o C# offrono da tempo un'API di riflessione, ma JavaScript non ne ha bisogno che sia un linguaggio dinamico. Si può sostenere che ES5 abbia già alcune funzionalità di riflessione, come Array.isArray() o Object.getOwnPropertyDescriptor(), che verrebbero considerate nelle altre lingue. ES2015 introduce un'API Reflection che ospiterà metodi futuri per questa categoria, rendendoli più facili da ragionare. Questo ha senso, in quanto l'oggetto è pensato come un prototipo di base piuttosto che come un bucket per i metodi di riflessione.

Utilizzando Reflect, possiamo migliorare il nostro precedente esempio di supereroe per un'adeguata intercettazione sul campo nelle mosse get e gettate 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

Il risultato è:

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

Un altro esempio è il caso in cui si potrebbe voler:

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

  • Aggiungi la possibilità di "salvare" le modifiche, ma solo se i dati sono stati effettivamente modificati (ipoteticamente perché l'operazione di salvataggio è molto costosa).

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 di API Reflect, consulta la sezione Proxies ES6 di Tagtree.

Polyfilling Object.observe()

Anche se diremo addio a Object.observe(), ora è possibile eseguire il polyfill utilizzando i proxy ES2015. Simon Blackwell ha scritto di recente un oggetto shim basato su proxy che vale la pena provare. Anche Erik Arvidsson risale al 2012 e scrisse anche una versione piuttosto completa con le specifiche.

Supporto del browser

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

Google ha rilasciato un polyfill limitato per il proxy. Questa opzione può essere utilizzata solo per wrapper generici, poiché può essere utilizzata solo per le proprietà proxy note al momento della creazione di un proxy.

Per approfondire