Presentación de los proxies de ES2015

Addy Osmani
Addy Osmani

Los proxies de ES2015 (en Chrome 49 y versiones posteriores) proporcionan a JavaScript una API de intercesión, lo que nos permite atrapar o interceptar todas las operaciones en un objeto de destino y modificar cómo funciona este objetivo.

Los proxies tienen una gran cantidad de usos, entre los que se incluyen los siguientes:

  • Intercepción
  • Virtualización de objetos
  • Administración de recursos
  • Creació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, que puede modificar el comportamiento original del objeto de destino de varias maneras útiles. El controlador contiene métodos de trampa opcionales (p. ej., .get(), .set() y .apply()) a los que se llama cuando se realiza la operación correspondiente en el proxy.

Intercepción

Comencemos por tomar un objeto simple 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 le aplica el proxy) y el segundo es el controlador (el proxy en sí). Aquí es donde podemos agregar hooks para nuestros métodos get, set y otros comportamientos.

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

Si 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 correctamente la obtención o el establecimiento de propiedades en el objeto de proxy generó una llamada a nivel de 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, que se reenvían a la trampa correspondiente.

La función de trampa 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. Por ejemplo, este es un proxy de reenvío sin operaciones que solo hace 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 el establecimiento de proxy de objetos simples, pero también podemos establecer un proxy de 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

Cómo identificar proxies

La identidad de un proxy se puede observar con los operadores de igualdad de JavaScript (== y ===). Como sabemos, cuando se aplican a dos objetos, estos operadores comparan las identidades de los objetos. En el siguiente ejemplo, se muestra este comportamiento. La comparación de dos proxies distintos muestra un valor falso a pesar de que los destinos subyacentes sean 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

Idealmente, no deberías poder distinguir un proxy de un objeto que no es un proxy, de modo que implementar un proxy no afecte realmente 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 los wrappers genéricos: proxies que unen otros objetos en el mismo “espacio” de dirección. También se mencionó la virtualización. Los objetos virtuales son proxies que emulan otros objetos sin que estos deban estar en el mismo espacio de direcciones. Entre los ejemplos, se incluyen los objetos remotos (que emulan objetos en otros espacios) y los futuros transparentes (que emulan resultados que aún no se calcularon).

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 solo se reenvía si la verificación se realiza correctamente. En el siguiente ejemplo de validación, se muestra lo siguiente:

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

Los ejemplos más complejos de este patrón pueden tener en cuenta todas las operaciones diferentes que los controladores de proxy pueden interceptar. Se podría imaginar una implementación que tenga 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 fácilmente, ya que cada operación puede tener que reenviarse de forma diferente. En un escenario ideal, si todas las operaciones pudieran canalizarse de forma 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 trampa única. Para ello, puedes implementar el controlador de 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. Por ejemplo, es posible que desees que un controlador registre operaciones, notifique a los observadores, arroje excepciones en lugar de mostrar un valor indefinido o redireccione operaciones a diferentes destinos para el almacenamiento. En estos casos, usar un proxy puede generar un resultado muy diferente al de 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 de destino a un código no confiable, se podrí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 en particular, puede revocar la referencia que separa el proxy de su destino. La membrana extendería este desprendimiento de forma recursiva a todos los objetos a los que se pueda acceder desde el destino original que se definió.

Cómo usar la reflexión con proxies

Reflect es un nuevo objeto integrado que proporciona métodos para operaciones interceptables de JavaScript, muy útiles para trabajar con proxies. De hecho, los métodos de reflexión son los mismos que los de los controladores de proxy.

Los lenguajes con tipos estáticos, como Python o C#, ofrecen una API de reflexión desde hace mucho tiempo, pero JavaScript no la necesita, ya que es un lenguaje dinámico. Se puede argumentar que ES5 ya tiene bastantes funciones de reflexión, como Array.isArray() o Object.getOwnPropertyDescriptor(), que se considerarían reflexión en otros lenguajes. ES2015 presenta una API de reflexión que alojará los métodos futuros de esta categoría, lo que facilitará su razonamiento. Esto tiene sentido, ya que Object está destinado a ser un prototipo base en lugar de un bucket para métodos de reflexión.

Con Reflect, podemos mejorar nuestro ejemplo anterior de superhéroe para interceptar correctamente el campo 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

Que genera lo siguiente:

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

Otro ejemplo es cuando se desea hacer lo siguiente:

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

  • Se agregó la capacidad de "guardar" los cambios, pero solo si los datos 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.

Polyfilling Object.observe()

Aunque le decimos adiós a Object.observe(), ahora es posible realizar la polyfill con proxies de ES2015. Simon Blackwell escribió recientemente un empalme de Object.observe() basado en proxy que vale la pena revisar. Erik Arvidsson también escribió una versión bastante completa según las especificaciones en 2012.

Navegadores compatibles

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

Google lanzó un polyfill limitado para Proxy. Solo se puede usar para wrappers genéricos, ya que solo puede usar proxy de propiedades conocidas en el momento en que se crea un proxy.

Lecturas adicionales