3 de julio de 2022

Proxy y Reflect

Un objeto Proxy envuelve (es un “wrapper”: envoltura, contenedor) a otro objeto e intercepta sus operaciones (como leer y escribir propiedades, entre otras). El proxy puede manejar estas operaciones él mismo o, en forma transparente permitirle manejarlas al objeto envuelto.

Los proxys son usados en muchas librerías y en algunos frameworks de navegador. En este artículo veremos muchas aplicaciones prácticas.

Proxy

La sintaxis:

let proxy = new Proxy(target, handler)
  • target – es el objeto a envolver, puede ser cualquier cosa, incluso funciones.
  • handler – configuración de proxy: un objeto que “atrapa”, métodos que interceptan operaciones. Ejemplos, la trampa get para leer una propiedad de target, la trampa set para escribir una propiedad en target, entre otras.

Cuando hay una operación sobre proxy, este verifica si hay una trampa correspondiente en handler. Si la trampa existe se ejecuta y el proxy tiene la oportunidad de manejarla, de otro modo la operación es ejecutada por target.

Como ejemplo para comenzar, creemos un proxy sin ninguna trampa:

let target = {};
let proxy = new Proxy(target, {}); // manejador vacío

proxy.test = 5; // escribiendo en el proxy (1)
alert(target.test); // 5, ¡la propiedad apareció en target!

alert(proxy.test); // 5, también podemos leerla en el proxy (2)

for(let key in proxy) alert(key); // test, la iteración funciona (3)

Como no hay trampas, todas las operaciones sobre proxy son redirigidas a target.

  1. Una operación de escritura proxy.test= establece el valor en target.
  2. Una operación de lectura proxy.test devuelve el valor desde target.
  3. La iteración sobre proxy devuelve valores de target.

Como podemos ver, sin ninguna trampa, proxy es un envoltorio transparente alrededor de target.

Proxy es un “objeto exótico” especial. No tiene propiedades propias. Con un manejador transparente redirige todas las operaciones hacia target.

Para activar más habilidades, agreguemos trampas.

¿Qué podemos interceptar con ellas?

Para la mayoría de las operaciones en objetos existe el denominado “método interno” en la especificación Javascript que describe cómo este trabaja en el más bajo nivel. Por ejemplo [[Get]]: es el método interno para leer una propiedad, [[Set]]: el método interno para escribirla, etcétera. Estos métodos solamente son usados en la especificación, no podemos llamarlos directamente por nombre.

Las trampas del proxy interceptan la invocación a estos métodos. Están listadas en la Especificación del proxy y en la tabla debajo.

Para cada método interno, existe una “trampa” en esta tabla: es el nombre del método que podemos agregar al parámetro handler de new Proxy para interceptar la operación:

Método interno Método manejador Cuándo se dispara
[[Get]] get leyendo una propiedad
[[Set]] set escribiendo una propiedad
[[HasProperty]] has operador in
[[Delete]] deleteProperty operador delete
[[Call]] apply llamado a función
[[Construct]] construct operador new
[[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf
[[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf
[[IsExtensible]] isExtensible Object.isExtensible
[[PreventExtensions]] preventExtensions Object.preventExtensions
[[DefineOwnProperty]] defineProperty Object.defineProperty, Object.defineProperties
[[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries
[[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entries
Invariantes

JavaScript impone algunas invariantes: condiciones que deben ser satisfechas por métodos internos y trampas.

La mayor parte de ellos son para devolver valores:

  • [[Set]] debe devolver true si el valor fue escrito correctamente, de otro modo false.
  • [[Delete]] debe devolver true si el valor fue borrado correctamente, de otro modo false.
  • …y otros, veremos más ejemplos abajo.

Existen algunas otras invariantes, como:

  • [[GetPrototypeOf]], aplicado al proxy, debe devolver el mismo valor que [[GetPrototypeOf]] aplicado al “target” del proxy. En otras palabras, leer el prototipo de un proxy debe devolver siempre el prototipo de su objeto target.

Las trampas pueden interceptar estas operaciones, pero deben seguir estas reglas.

Las invariantes aseguran un comportamiento correcto y consistente de características de lenguaje. La lista completa de invariantes está en la especificación. Probablemente no las infringirás si no estás haciendo algo retorcido.

Veamos cómo funciona en ejemplos prácticos.

Valores “por defecto” con la trampa “get”

Las trampas más comunes son para leer y escribir propiedades.

Para interceptar una lectura, el handler debe tener un método get(target, property, receiver).

Se dispara cuando una propiedad es leída, con los siguientes argumentos:

  • target – “objetivo”, es el objeto pasado como primer argumento a new Proxy,
  • property – nombre de la propiedad,
  • receiver – si la propiedad objetivo es un getter, el receiver es el objeto que va a ser usado como this en su llamado. Usualmente es el objeto proxy mismo (o un objeto que hereda de él, si heredamos desde proxy). No necesitamos este argumento ahora mismo, así que se verá en más detalle luego.

Usemos get para implementar valores por defecto a un objeto.

Crearemos un arreglo numérico que devuelve 0 para valores no existentes.

Lo usual al tratar de obtener un ítem inexistente de un array es obtener undefined, pero envolveremos un array normal en un proxy que atrape lecturas y devuelva 0 si no existe tal propiedad:

let numbers = [0, 1, 2];

numbers = new Proxy(numbers, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    } else {
      return 0; // valor por defecto
    }
  }
});

alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (porque no existe tal ítem)

Como podemos ver, es muy fácil de hacer con una trampa get.

Podemos usar