Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:programacion_avanzada_javascript:proxies

¡Esta es una revisión vieja del documento!


Proxies

Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.

Introducción

Una de las características nuevas más desconocidas para muchos desarrolladores que incorpora ECMAScript 2015 son los proxies.

Un proxy es un objeto que permite interceptar las operaciones (tales como llamar a un método o consultar una propiedad) que se realizan sobre un objeto y realizar determinadas acciones.

Los proxies permiten escenarios avanzados, imposibles en ES5, ya que pueden interceptar llamadas a métodos o propiedades realizados contra otros objetos, de forma totalmente transparente para quien realiza esas operaciones.

Las principales aplicaciones de los proxies son todas avanzadas y no se usarán a menudo, pero son una herramienta muy útil para conseguir, entre otras cuestiones avanzadas:

  • La intercepción de llamadas a propiedades y métodos de objetos para modificar su comportamiento.
  • El control y autorización de llamadas para seguridad y gestión del uso de objetos y funciones.
  • Traza y log para seguimiento de llamadas de código.
  • Creación de contratos para uso de código.
  • La virtualización de objetos de modo que podamos crear objetos virtuales que utilicen a otros objetos por debajo pero modificando su comportamiento o actuando sobre varios objetos o propiedades al mismo tiempo.

En este módulo vamos cómo podemos crear los proxies y qué escenarios nos permiten solucionar.

Los proxies requieren soporte del motor de JavaScript por lo que no se pueden transpilar ni usar ningún polyfill para simularlos.

Creación de proxies

Crear un proxy no es muy complicado. Lo primero es tener presente que, dado que un proxy intercepta operaciones sobre un objeto, vamos a necesitar lógicamente un objeto al que interceptar. Este objeto no tiene que tener nada especial:

let obj = {
   get value() { return 42;}
}

Hemos definido un objeto obj con un getter (value) que devuelve el valor 42. Vamos ahora a crear un proxy que intercepte las llamadas a este getter y que imprima por consola que alguien ha llamado al getter:

let proxy = new Proxy(obj,
    {
        get(target, key, receiver) {
            console.log(`Prop ${key} was called`);
            return target[key];
        }    
    });
let v = proxy.value;
console.log(v);

Vamos a diseccionar el código. Para crear un proxy debemos crear un objeto del tipo Proxy. Para ello debemos pasar dos parámetros:

  • El objeto cuyas operaciones queremos interceptar
  • Un objeto que indica qué acciones queremos interceptar y qué queremos hacer cuando las interceptemos.

En este caso el objeto pasado (que se suele denominar handler) indica que queremos interceptar todas las operaciones que impliquen llamar a un getter.

Luego en la intercepción nos limitamos a imprimir por la consola cuál es la propiedad accedida y devolvemos el valor correcto de dicha propiedad (podríamos en efecto devolver cualquier otra cosa si quisiéramos).

Finalmente llamamos al getter value del proxy. Observa como el proxy no tiene definido dicho getter, pero se intercepta la operación y se ejecuta el código del proxy, el cual puede (si quiere) terminar llamando al objeto final.

Es importante observar que el getter (value) se llama sobre el proxy, no sobre el objeto real.

Enseguida lo vamos a ver en la práctica y se entenderá mucho mejor. Pero antes analicemos las posibles intercepciones en los proxies.

Intercepciones básicas

Vamos a ver qué podemos interceptar con un proxy. Las intercepciones básicas son llamadas a getters de propiedad, a setters y a métodos.

Getters de propiedad

Ya lo hemos visto en la lección anterior: basta con definir un método get en el handler del proxy. Este método recibe tres parámetros:

  • target: es el propio objeto interceptado por el proxy
  • key: el nombre de la propiedad
  • receiver: el valor de la propiedad en el objeto

Así p. ej. podríamos crear un proxy que crease una propiedad “fantasma” json que devolviese una representación en json del objeto:

let obj = {v: 42};
 
let proxy = new Proxy(obj, {
   get (target, key, receiver) {
       if (key === "json") {
           return JSON.stringify(target);
       }
       else {
           return target[key];
       }
   }
});
 
let str = proxy.json;       // "{"v":42}"

Setters de propiedad

Para interceptar un setter de una propiedad el handler debe definir un método set. Los parámetros de este método set son los mismos que el del método get usado para interceptar un getter, con la excepción de que receiver ahora es el nuevo valor que se quiere asignar a la propiedad (no el que tiene actualmente asignado el objeto en esa propiedad, como antes).

Vamos a crear un proxy que obligue a que todas las propiedades de un objeto tengan un valor de tipo cadena:

let obj = {v: "una cadena"};
let proxy = new Proxy(obj, {
   set (target, key, receiver) {
       if (typeof(receiver) === "string") {
           target[key] = receiver;
       }
   }
});
 
proxy.v = 42;
proxy.v;            // Sigue teniendo el valor original, "una cadena", pues no se puede asignar un número
proxy.v = "ahora sí";
proxy.v;            // "ahora sí"

El uso de proxies es muy sencillo y nos da una potencia brutal, permitiéndonos realizar tareas que sin ellos serían mucho más difíciles.

Llamadas a métodos

Para llamar a métodos se usa también el método get del handler. De hecho JavaScript no distingue entre getters de propiedades y llamadas a métodos.

Cuando tenemos un código tipo o.f(), primero se llama al getter de la propiedad f del objeto o y si el valor devuelto es una función se invoca a dicha función (en caso contrario el motor de JavaScript da un error). Así, lo que debemos tener claro es que en el handler debemos devolver una función, porque sino, éste recibirá un error:

var obj = {
v: 42,
inc(value) {this.v += value; return this.v; }
}
 
let proxy = new Proxy(obj, {
   get (target, key, receiver) {
      var prop = target[key];
      if (typeof(prop) === "function") {
          return function(...args) {
              let result = prop.apply(this, args);
              console.log(`${key}(${JSON.stringify(args)}) -> ${result != undefined ?  JSON.stringify(result) : undefined}`);
              return result;
          }
      }
      else return target[key];
   }
});

Este código intercepta todas las llamadas a todos métodos y las imprime por la consola. Observa el uso de …args en la función devuelta por el proxy. Al tener un solo parámetro rest, eso lo que hace es que args sea un array con los valores pasados a la función.

De este modo cuando hacemos proxy.inc(10) sucede lo siguiente:

  • Se llama al método get del handler
  • Este método obtiene el valor de la propiedad indicada (inc)
  • Si la propiedad es una función (en este caso lo es), devuelve otra función que llama a la función del objeto original (usando apply y luego imprime la llamada por la consola (y finalmente devuelve el resultado de haber llamado a la función original). La idea es que llamar a proxy.inc(10) termina llamando realmente a la función interna (que tiene el parámetro rest args) con el valor de 10. Por lo tanto dentro de esa función el valor de args es un array con un elemento: 10. Así podemos usar fácilmente apply para obtener el valor real y luego hacer lo que queramos.
  • Si la propiedad llamada no fuese una función (p. ej. hiciéramos proxy.v) pues directamente se devuelve el valor de invocar a dicha propiedad en el objeto real.

DEMO: Interceptando un getter a propiedad

Lo importante de los proxies es entender que envuelven un objeto y luego nos permiten interceptar llamadas a las propiedades y métodos del objeto y terminar delegando esta llamada a la propiedad real del objeto o devolver cualquier otra cosa a quien hace la llamada.

Partimos de un objeto sencillo:

var obj = {
    prop: 42
}

Ahora podemos tener una función así:

function foo(p) {
    console.log(p.prop)
}

Al usarla:

foo(obj); // 42

Vamos a crear un proxy para envolver el objeto:

let proxy = new Proxy(obj, {
    get(target, key, receiver) {
        console.log(target, key, receiver);
    }
})

Ahora a la función debemos pasarle el proxy, no el objeto original:

foo(proxy); // Object { prop: 42 } prop Object { prop: 42 }

Modificamos el proxy:

let proxy = new Proxy(obj, {
    get(target, key, receiver) {
        if (key == "prop") {
            return target[key];
        } else {
            return NaN;
        }
 
    }
})
foo(proxy); // 42

Si modificamos la función foo tal que así:

function foo(p) {
    console.log(p.prop)
    console.log(p.cualquierCosa);
}

Y ejecutamos:

foo(proxy); // 42
            // NaN

Hemos visto cómo el proxy nos ha permitido envolver de manera sencilla un objeto y expandir su comportamiento sin necesidad de tocar el objeto original.

Ejercicio propuesto

Proxies a objetos de tipo función

DEMO: Creando un "congelador" de objetos - Parte 1

DEMO: Creando un "congelador" de objetos - Parte 2

DEMO: Creando un "congelador" de objetos - Parte 3

informatica/programacion/cursos/programacion_avanzada_javascript/proxies.1730118504.txt.gz · Última modificación: por tempwin