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 trampagetpara leer una propiedad detarget, la trampasetpara escribir una propiedad entarget, 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.
- Una operación de escritura
proxy.test=establece el valor entarget. - Una operación de lectura
proxy.testdevuelve el valor desdetarget. - La iteración sobre
proxydevuelve valores detarget.
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 |
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 devolvertruesi el valor fue escrito correctamente, de otro modofalse.[[Delete]]debe devolvertruesi el valor fue borrado correctamente, de otro modofalse.- …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 anew Proxy,property– nombre de la propiedad,receiver– si la propiedad objetivo es un getter, elreceiveres el objeto que va a ser usado comothisen su llamado. Usualmente es el objetoproxymismo (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