Presentación de los proxies de ES2015

Addy Osmani
Addy Osmani

Los proxies de ES2015 (en Chrome 49 y versiones posteriores) le proporcionan a JavaScript una API de intercesión, lo que nos permite capturar o interceptar todas las operaciones en un objeto de destino y modificar la forma en que opera este destino.

Los proxies tienen muchos usos, incluidos los siguientes:

  • Intercepción
  • Virtualización de objetos
  • Administración de recursos
  • Generación de perfiles o registro para la depuración
  • Seguridad y control de acceso
  • Contratos para el uso de objetos

La API de Proxy contiene un constructor de proxy que toma un objeto de destino designado y un objeto de controlador.

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

El controlador controla el comportamiento de un proxy, el cual puede modificar el comportamiento original del objeto target de varias maneras útiles. El controlador contiene métodos trap opcionales (p. ej., .get(), .set(), .apply()) que se llaman cuando se realiza la operación correspondiente en el proxy.

Intercepción

Comencemos por tomar un objeto sin formato y agregarle un middleware de intercepción con la API de Proxy. Recuerda que el primer parámetro que se pasa al constructor es el objetivo (el objeto al que se envía un proxy) y el segundo es el controlador (el proxy en sí). Aquí es donde podemos agregar hooks para nuestros métodos get, métodos set y otro comportamiento.

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

Cuando ejecutamos el código anterior en Chrome 49, obtenemos lo siguiente:

get was called for: power  
"Flight"

Como podemos ver en la práctica, realizar la obtención de propiedades o el conjunto de propiedades en el objeto del proxy dio como resultado una llamada de nivel meta a la trampa correspondiente en el controlador. Las operaciones del controlador incluyen lecturas de propiedades, asignación de propiedades y aplicación de funciones, las cuales se reenvían a la trampa correspondiente.

La función trap puede, si lo desea, implementar una operación de forma arbitraria (p. ej., reenviar la operación al objeto de destino). Esto es lo que sucede de forma predeterminada si no se especifica una trampa. P.ej., aquí hay un proxy de reenvío no-op que hace precisamente lo siguiente:

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

Analizamos cómo crear proxies de objetos sin formato, pero podemos usar un proxy con la misma facilidad en un objeto de función, en el que una función es nuestro objetivo. Esta vez, usaremos la trampa 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

Identifica los proxies

La identidad de un proxy se puede observar con los operadores de igualdad de JavaScript (== y ===). Como sabemos, cuando se aplica a dos objetos, estos operadores comparan las identidades de los objetos. En el siguiente ejemplo, se demuestra este comportamiento. La comparación de dos proxies distintos arroja un resultado falso, a pesar de que los objetivos subyacentes son los mismos. De manera similar, el objeto de destino es diferente de cualquiera de sus proxies:

// Continuing previous example

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

Lo ideal es que no puedas distinguir un proxy de un objeto no proxy para que la implementación de un proxy no afecte el resultado de tu app. Esta es una de las razones por las que la API de proxy no incluye una forma de verificar si un objeto es un proxy ni proporciona trampas para todas las operaciones en objetos.

Casos de uso

Como se mencionó, los proxies tienen una amplia variedad de casos de uso. Muchos de los anteriores, como el control de acceso y la generación de perfiles, se incluyen en Wrappers genéricos: proxies que unen otros objetos en el mismo "espacio" de la dirección. También se mencionó la virtualización. Los objetos virtuales son proxies que emulan otros objetos sin necesidad de que esos objetos estén en el mismo espacio de direcciones. Entre los ejemplos, se incluyen objetos remotos (que emulan objetos en otros espacios) y futuros transparentes (que emulan resultados que aún no se procesaron).

Proxies como controladores

Un caso de uso bastante común para los controladores de proxy es realizar verificaciones de validación o control de acceso antes de realizar una operación en un objeto unido. La operación se reenvía solo si la verificación se realiza de forma correcta. El siguiente ejemplo de validación demuestra esto:

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

En los ejemplos más complejos de este patrón, se pueden tener en cuenta todos los diferentes controladores del proxy de operaciones que pueden interceptar. Imagina una implementación que tiene que duplicar el patrón de verificación de acceso y reenviar la operación en cada trampa.

Esto puede ser difícil de abstraer con facilidad, ya que es posible que cada op tenga que reenviarse de manera diferente. En una situación perfecta, si todas las operaciones pudieran distribuirse de manera uniforme a través de una sola trampa, el controlador solo tendría que realizar la verificación de validación una vez en la única trampa. Para ello, implementa el controlador del proxy como un proxy. Lamentablemente, esto está fuera del alcance de este artículo.

Extensión de objetos

Otro caso de uso común para los proxies es extender o redefinir la semántica de las operaciones en objetos. Podrías, por ejemplo, que desees que un controlador registre operaciones, notifique a los observadores, genere excepciones en lugar de mostrar resultados indefinidos o redireccione las operaciones a diferentes destinos para el almacenamiento. En estos casos, usar un proxy puede generar un resultado muy diferente al usar el objeto de destino.

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

Control de acceso

El control de acceso es otro buen caso de uso para los proxies. En lugar de pasar un objeto objetivo a un fragmento de código que no es de confianza, se podía pasar su proxy envuelto en una especie de membrana protectora. Una vez que la app considere que el código no confiable completó una tarea particular, puede revocar la referencia que desconecta el proxy de su destino. La membrana extendería esta separación de manera recursiva a todos los objetos a los que se pueda acceder desde el objetivo original definido.

Usa la reflexión con proxies

Reflect es un nuevo objeto integrado que proporciona métodos para operaciones de JavaScript interceptables, muy útil a la hora de trabajar con proxies. De hecho, los métodos de Reflect son los mismos que los de los controladores de proxy.

Desde hace mucho tiempo, los lenguajes de escritura estática, como Python o C#, ofrecen una API de reflexión, pero JavaScript no necesita una como lenguaje dinámico. Se podría argumentar que ES5 ya tiene varias funciones de reflexión, como Array.isArray() o Object.getOwnPropertyDescriptor(), que se considerarían reflexión en otros idiomas. ES2015 presenta una API de Reflection que alojará métodos futuros para esta categoría, lo que facilitará su razonamiento. Esto tiene sentido, ya que el objeto está destinado a ser un prototipo de base en lugar de un bucket para los métodos de reflexión.

Con Reflect, podemos mejorar nuestro ejemplo anterior de superhéroes para la intercepción adecuada de campos en nuestras trampas de get y set de la siguiente manera:

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

Esto genera el siguiente resultado:

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

Otro ejemplo es aquel en el que podrías querer hacer lo siguiente:

  • Une una definición de proxy a un constructor personalizado para evitar crear manualmente un proxy nuevo cada vez que queramos trabajar con una lógica específica.

  • Se agregó la capacidad de "guardar" cambios, pero solo si los datos realmente se modificaron (hipotéticamente debido a que la operación de guardado es muy 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();

Para obtener más ejemplos de la API de Reflect, consulta Proxies de ES6 de Tagtree.

Cómo policompletar objetos.observa()

Si bien daremos de adiós a Object.observe(), ahora es posible realizar un polyfill con los proxies ES2015. Simon Blackwell escribió recientemente un corrección de compatibilidad de Object.observa() basado en proxy que vale la pena echarle un vistazo. Erik Arvidsson también escribió una versión bastante completa con las especificaciones hasta 2012.

Navegadores compatibles

Los proxies de ES2015 son compatibles con Chrome 49, Opera, Microsoft Edge y Firefox. Safari ha tenido varios indicadores públicos con respecto a la función, pero seguimos siendo optimistas. Reflect está disponible en Chrome, Opera y Firefox, y se encuentra en desarrollo para Microsoft Edge.

Google lanzó un polyfill limitado para proxy. Esto solo se puede utilizar para wraps genéricos, ya que solo puede usar un proxy en las propiedades conocidas en el momento en que se crea un proxy.

Lecturas adicionales