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 Proxy para implementar cualquier lógica para valores âpor defectoâ.
Supongamos que tenemos un diccionario con frases y sus traducciones:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined
Por ahora, si no existe la frase, la lectura de dictionary devuelve undefined. Pero en la práctica dejar la frase sin traducir es mejor que undefined. Asà que hagamos que devuelva la frase sin traducir en vez de undefined.
Para lograr esto envolvemos dictionary en un proxy que intercepta las operaciones de lectura:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // intercepta la lectura de una propiedad en dictionary
if (phrase in target) { // si existe en el diccionario
return target[phrase]; // devuelve la traducción
} else {
// caso contrario devuelve la frase sin traducir
return phrase;
}
}
});
// ¡Busque frases en el diccionario!
// En el peor caso, no serán traducidas.
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (sin traducir)
Nota cómo el proxy sobrescribe la variable:
dictionary = new Proxy(dictionary, ...);
El proxy debe reemplazar completamente al objeto âtargetâ que envolvió: nadie debe jamás hacer referencia al objeto target saltando tal envoltura. De otro modo serÃa fácil desbaratarlo.
Validación con la trampa âsetâ
Digamos que queremos un array exclusivamente para números. Si se agrega un valor de otro tipo, deberÃa dar un error.
La trampa set se dispara cuando una propiedad es escrita.
set(target, property, value, receiver):
targetâ objetivo, el objeto pasado como primer argumento anew Proxy,propertyâ nombre de la propiedad,valueâ valor de la propiedad,receiverâ similar para la trampaget, de importancia solamente en propiedades setter.
La trampa set debe devolver true si la escritura fue exitosa, y false en caso contrario (dispara TypeError).
Usémoslo para validar valores nuevos:
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // para interceptar la escritura de propiedad
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // añadido correctamente
numbers.push(2); // añadido correctamente
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError ('set' en el proxy devolvió false)
alert("Esta linea nunca es alcanzada (error en la lÃnea de arriba)");
Ten en cuenta: ¡la funcionalidad integrada de los arrays aún funciona! Los valores son añadidos por push. La propiedad length se autoincrementa cuando son agregados valores. Nuestro proxy no rompe nada.
No necesitamos sobrescribir métodos de valor añadido como push, unshift y demás para agregar los chequeos allÃ, porque internamente ellos usan la operación [[Set]] que es interceptada por el proxy.
Entonces el código es limpio y conciso.
trueComo dijimos antes, hay invariantes que se deben mantener.
Para set, debe devolver true si la escritura fue correcta.
Si olvidamos hacerlo o si devolvemos false, la operación dispara TypeError.
Iteración con âownKeysâ y âgetOwnPropertyDescriptorâ
Object.keys, el bucle for..in, y la mayorÃa de los demás métodos que iteran sobre las propiedades de objeto usan el método interno [[OwnPropertyKeys]] (interceptado por la trampa ownKeys) para obtener una lista de propiedades .
Tales métodos difieren en detalles:
Object.getOwnPropertyNames(obj)devuelve claves no symbol.Object.getOwnPropertySymbols(obj)devuelve claves symbol.Object.keys/values()devuelve claves/valores no symbol con indicadorenumerable(los indicadores de propiedad fueron explicados en el artÃculo Indicadores y descriptores de propiedad).for..initera sobre claves no symbol con el indicadorenumerable, y también claves prototÃpicas.
â¦Pero todos ellos comienzan con aquella lista.
En el ejemplo abajo usamos la trampa ownKeys para hacer el bucle for..in sobre user. También usamos Object.keys y Object.values para pasar por alto las propiedades que comienzan con un guion bajo _:
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// el filtro en "ownKeys" descarta _password
for(let key in user) alert(key); // name, then: age
// el mismo efecto con estos métodos:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30
Hasta ahora, funciona.
Aunque si devolvemos una clave que no existe en el objeto, Object.keys no la listará:
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(user) ); // <vacÃo>
¿Por qué? La razón es simple: Object.keys devuelve solamente propiedades con el indicador enumerable. Para verificarlo, llama el método interno [[GetOwnProperty]] en cada propiedad para obtener su descriptor. Y aquÃ, como no hay propiedad, su descriptor está vacÃo, no existe el indicador enumerable, entonces lo salta.
Para que Object.keys devuelva una propiedad, necesitamos que, o bien exista en el objeto, con el indicador enumerable, o interceptamos llamadas a [[GetOwnProperty]] (la trampa getOwnPropertyDescriptor lo hace), y devolver un descriptor con enumerable: true.
Aquà un ejemplo de ello:
let user = { };
user = new Proxy(user, {
ownKeys(target) { // llamado una vez para obtener la lista de propiedades
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // llamada para cada propiedad
return {
enumerable: true,
configurable: true
/* ...otros indicadores, probablemente "value:..." */
};
}
});
alert( Object.keys(user) ); // a, b, c
Tomemos nota de nuevo: solamente necesitamos interceptar [[GetOwnProperty]] si la propiedad está ausente en el objeto.
Propiedades protegidas con âdeletePropertyâ y otras trampas
Hay una convención extendida: las propiedades y los métodos que comienzan con guion bajo _ son de uso interno. Ellos no deberÃan ser accedidos desde fuera del objeto.
Aunque es técnicamente posible:
let user = {
name: "John",
_password: "secreto"
};
alert(user._password); // secreto
Usemos proxy para prevenir cualquier acceso a propiedades que comienzan con _.
Necesitaremos las trampas:
getpara arrojar un error al leer tal propiedad,setpara arrojar un error al escribirla,deletePropertypara arrojar un error al eliminar,ownKeyspara excluir propiedades que comienzan con_defor..iny métodos comoObject.keys.
Aquà el código:
let user = {
name: "John",
_password: "***"
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Acceso denegado");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
},
set(target, prop, val) { // para interceptar la escritura de la propiedad
if (prop.startsWith('_')) {
throw new Error("Acceso denegado");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) { // para interceptar la eliminación de la propiedad
if (prop.startsWith('_')) {
throw new Error("Acceso denegado");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // para interceptar su listado
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" no permite leer _password
try {
alert(user._password); // Error: Acceso denegado
} catch(e) { alert(e.message); }
// "set" no permite escribir _password
try {
user._password = "test"; // Error: Acceso denegado
} catch(e) { alert(e.message); }
// "deleteProperty" no permite eliminar _password
try {
delete user._password; // Error: Acceso denegado
} catch(e) { alert(e.message); }
// "ownKeys" filtra descartando _password
for(let key in user) alert(key); // name
Nota el importante detalle en la trampa get, en la lÃnea (*):
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
¿Por qué necesitamos una función para llamar value.bind(target)?
La razón es que los métodos de objeto, como user.checkPassword(), deben ser capaces de acceder a _password:
user = {
// ...
checkPassword(value) {
// método de objeto debe poder leer _password
return value === this._password;
}
}
Un llamado a user.checkPassword() hace que el objeto target user sea this (el objeto antes del punto se vuelve this), entonces cuando trata de acceder a this._password, la trampa get se activa (se dispara en cualquier lectura de propiedad) y arroja un error.
Entonces vinculamos (bind) el contexto de los métodos al objeto original, target, en la lÃnea (*). Asà futuros llamados usarán target como this, sin trampas.
Esta solución usualmente funciona, pero no es ideal, porque un método podrÃa pasar el objeto original hacia algún otro lado y lo habremos arruinado: ¿dónde está el objeto original, y dónde el apoderado?
Además, un objeto puede ser envuelto por proxys muchas veces (proxys múltiples pueden agregar diferentes ajustes al objeto), y si pasamos un objeto no envuelto por proxy a un método, puede haber consecuencias inesperadas.
Por lo tanto, tal proxy no deberÃa usarse en todos lados.
Los motores de JavaScript moderno soportan en las clases las propiedades privadas, aquellas con el prefijo #. Estas son descritas en el artÃculo Propiedades y métodos privados y protegidos.. No requieren proxys.
Pero tales propiedades tienen sus propios problemas. En particular, ellas no se heredan.
âIn rangeâ con la trampa âhasâ
Veamos más ejemplos.
Tenemos un objeto range:
let range = {
start: 1,
end: 10
};
Queremos usar el operador in para verificar que un número está en el rango, range.
La trampa has intercepta la llamada in.
has(target, property)
targetâ objetivo, el objeto pasado como primer argumento anew Proxy,propertyâ nombre de propiedad
Aquà el demo:
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
Bonita azúcar sintáctica, ¿no es cierto? Y muy simple de implementar.
Envolviendo funciones: "apply"
Podemos envolver un proxy a una función también.
La trampa apply(target, thisArg, args) maneja llamados a proxy como función:
targetes el objeto/objetivo (en JavaScript, la función es un objeto),thisArges el valor dethis.argses una lista de argumentos.
Por ejemplo, recordemos el decorador delay(f, ms) que hicimos en el artÃculo Decoradores y redirecciones, call/apply.
En ese artÃculo lo hicimos sin proxy. Un llamado a delay(f, ms) devolvÃa una función que redirigÃa todos los llamados a f después de ms milisegundos.
Aquà la version previa, implementación basada en función:
function delay(f, ms) {
// devuelve un envoltorio que pasa el llamado a f después del timeout
return function() { // (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// después de esta envoltura, los llamados a sayHi serán demorados por 3 segundos
sayHi = delay(sayHi, 3000);
sayHi("John"); // Hello, John! (después de 3 segundos)
Como ya hemos visto, esto mayormente funciona. La función envoltorio (*) ejecuta el llamado después del lapso.
Pero una simple función envoltura (wrapper) no redirige operaciones de lectura y escritura ni ninguna otra cosa. Una vez envuelta, el acceso a las propiedades de la función original (name, length) se pierde:
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1 (length, longitud, en una función es la cantidad de argumentos en su declaración)
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 0 (en la declaración de envoltorio hay cero argumentos)
El Proxy es mucho más poderoso, porque redirige todo lo que no maneja al objeto envuelto âtargetâ.
Usemos Proxy en lugar de una función envoltura:
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 1 (*) el proxy redirige la operación "get length" al objeto target
sayHi("John"); // Hello, John! (después de 3 segundos)
El resultado es el mismo, pero ahora no solo las llamadas sino todas las operaciones son redirigidas a la función original. Asà sayHi.length se devuelve correctamente luego de la envoltura en la lÃnea (*).
Obtuvimos una envoltura âenriquecidaâ.
Existen otras trampas. La lista completa está en el principio de este artÃculo. Su patrón de uso es similar al de arriba.
Reflect
Reflect es un objeto nativo que simplifica la creación de Proxy.
Se dijo previamente que los métodos internos como [[Get]], [[Set]] son únicamente para la especificación, que no pueden ser llamados directamente.
El objeto Reflect hace de alguna manera esto posible. Sus métodos son envoltorios mÃnimos alrededor del método interno.
Aquà hay ejemplos de operaciones y llamados a Reflect que hacen lo mismo:
| Operación | Llamada Reflect |
Método interno |
|---|---|---|
obj[prop] |
Reflect.get(obj, prop) |
[[Get]] |
obj[prop] = value |
Reflect.set(obj, prop, value) |
[[Set]] |
delete obj[prop] |
Reflect.deleteProperty(obj, prop) |
[[Delete]] |
new F(value) |
Reflect.construct(F, value) |
[[Construct]] |
| ⦠| ⦠| ⦠|
Por ejemplo:
let user = {};
Reflect.set(user, 'name', 'John');
alert(user.name); // John
En particular, Reflect nos permite llamar a los operadores (new, delete, â¦) como funciones (Reflect.construct, Reflect.deleteProperty, â¦). Esta es una capacidad interesante, pero hay otra cosa importante.
Para cada método interno atrapable por Proxy, hay un método correspondiente en Reflect con el mismo nombre y argumentos que la trampa Proxy.
Entonces podemos usar Reflect para redirigir una operación al objeto original.
En este ejemplo, ambas trampas get y set transparentemente (como si no existieran) reenvÃan las operaciones de lectura y escritura al objeto, mostrando un mensaje:
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // muestra "GET name"
user.name = "Pete"; // muestra "SET name=Pete"
AquÃ:
Reflect.getlee una propiedad de objeto.Reflect.setescribe una propiedad de objeto y devuelvetruesi fue exitosa,falsesi no lo fue.
Eso es todo, asà de simple: si una trampa quiere dirigir el llamado al objeto, es suficiente con el llamado a Reflect.<method> con los mismos argumentos.
En la mayorÃa de los casos podemos hacerlo sin Reflect, por ejemplo, leer una propiedad Reflect.get(target, prop, receiver) puede ser reemplazado por target[prop]. Aunque hay importantes distinciones.
Proxy en un getter
Veamos un ejemplo que demuestra por qué Reflect.get es mejor. Y veremos también por qué get/set tiene el tercer argumento receiver que no usamos antes.
Tenemos un objeto user con la propiedad _name y un getter para ella.
Aquà hay un proxy alrededor de él:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop];
}
});
alert(userProxy.name); // Guest
La trampa get es âtransparenteâ aquÃ, devuelve la propiedad original, y no hace nada más. Esto es suficiente para nuestro ejemplo.
Todo se ve bien. Pero hagamos el ejemplo un poco más complejo.
Después de heredar otro objeto admin desde user, podemos observar el comportamiento incorrecto:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // (*) target = user
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// Esperado: Admin
alert(admin.name); // salida: Guest (?!?)
¡Leer admin.name deberÃa devolver "Admin", no "Guest"!
¿Qué es lo que pasa? ¿Acaso hicimos algo mal con la herencia?
Pero si quitamos el proxy, todo funciona como se espera.
En realidad el problema está en el proxy, en la lÃnea (*).
-
Cuando leemos
admin.name, como el objetoadminno tiene su propia propiedad, la búsqueda va a su prototipo. -
El prototipo es
userProxy. -
Cuando se lee la propiedad
namedel proxy, se dispara su trampagety devuelve desde el objeto original comotarget[prop]en la lÃnea(*).Un llamado a
target[prop], cuandopropes un getter, ejecuta su código en el contextothis=target. Entonces el resultado esthis._namedesde el objeto originaltarget, que es: desdeuser.
Para arreglar estas situaciones, necesitamos receiver, el tercer argumento de la trampa get. Este mantiene el this correcto para pasarlo al getter. Que en nuestro caso es admin.
¿Cómo pasar el contexto para un getter? Para una función regular podemos usar call/apply, pero es un getter, no es âllamadoâ, solamente accedido.
Reflect.get hace eso. Todo funcionará bien si lo usamos.
Aquà la variante corregida:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
Ahora receiver, que mantiene una referencia al this correcto (que es admin), es pasado al getter usando Reflect.get en la lÃnea (*).
Podemos reescribir la trampa aún más corta:
get(target, prop, receiver) {
return Reflect.get(...arguments);
}
Los llamados de Reflect fueron nombrados exactamente igual a las trampas y aceptan los mismos argumentos. Fueron especÃficamente diseñados asÃ.
Entonces, return Reflect... brinda una forma segura y âno cerebralâ de redirigir la operación y asegurarse de que no olvidamos nada relacionado a ello.
Limitaciones del proxy
Proxy brinda una manera única de alterar o ajustar el comportamiento de objetos existentes al más bajo nivel. Pero no es perfecto. Hay limitaciones.
Objetos nativos: slots internos
Muchos objetos nativos, por ejemplo Map, Set, Date, Promise, etc, hacen uso de los llamados âslots internosâ.
Los slots (hueco, celda) son como propiedades; pero están reservados para uso interno, con propósito de especificación únicamente. Por ejemplo, Map almacena items en el slot interno [[MapData]]. Los métodos nativos los acceden directamente, sin usar los métodos internos [[Get]]/[[Set]]. Entonces Proxy no puede interceptar eso.
¿Qué importa? ¡De cualquier manera son internos!
Bueno, hay un problema. Cuando se envuelve un objeto nativo el proxy no tiene acceso a estos slots internos, entonces los métodos nativos fallan.
Por ejemplo:
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // Error
Internamente, un Map almacena todos los datos en su slot interno [[MapData]]. El proxy no tiene tal slot. El método nativo Map.prototype.set trata de acceder a la propiedad interna this.[[MapData]], pero como this=proxy, no puede encontrarlo en proxy y simplemente falla.
Afortunadamente, hay una forma de arreglarlo:
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 1);
alert(proxy.get('test')); // 1 (¡Funciona!)
Ahora funciona bien porque la trampa get vincula las propiedades de la función, tales como map.set, al objeto target mismo (map).
A diferencia del ejemplo previo, el valor de this dentro de proxy.set(...) no será proxy sino el map original. Entonces, cuando la implementación interna de set trata de acceder al slot interno this.[[MapData]], lo logra.
Array no tiene slots internosUna excepción notable: El objeto nativo Array no tiene slots internos. Esto es por razones históricas, ya que apareció hace tanto tiempo.
Asà que no hay problema en usar proxy con un array.
Campos privados
Algo similar ocurre con los âcampos privadosâ usados en las clases.
Por ejemplo, el método getName() accede a la propiedad privada #name y falla cuando lo proxificamos:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // Error
La razón es que los campos privados son implementados usando slots internos. JavaScript no usa [[Get]]/[[Set]] cuando accede a ellos.
En la llamada a getName(), el valor de this es el proxy userque no tiene el slot con campos privados.
De nuevo, la solución de vincular el método hace que funcione:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
alert(user.getName()); // Guest
Dicho esto, la solución tiene su contra, explicada previamente: expone el objeto original al método, potencialmente permite ser pasado más allá y dañar otra funcionalidad del proxy.
Proxy != target
El proxy y el objeto original son objetos diferentes. Es natural, ¿cierto?
Asà que si usamos el objeto original como clave y luego lo hacemos proxy, entonces el proxy no puede ser hallado:
let allUsers = new Set();
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
let user = new User("John");
alert(allUsers.has(user)); // true
user = new Proxy(user, {});
alert(allUsers.has(user)); // false
Como podemos ver, después del proxy no podemos hallar user en el set allUsers porque el proxy es un objeto diferente.
===Los proxys pueden interceptar muchos operadores; tales como new (con construct), in (con has), delete (con deleteProperty) y otros.
Pero no hay forma de interceptar un test de igualdad estricta entre objetos. Un objeto es estrictamente igual únicamente a sà mismo y a ningún otro valor.
Por lo tanto todas las operaciones y clases nativas que hacen una comparación estricta de objetos diferenciarán entre el objeto original y su proxy. No hay reemplazo transparente aquÃâ¦
Proxy revocable
Un proxy revocable es uno que puede ser deshabilitado.
Digamos que tenemos un recurso al que quisiéramos poder cerrar en cualquier momento.
Podemos envolverlo en un proxy revocable sin trampas. Tal proxy dirigirá todas las operaciones al objeto, y podemos deshabilitarlo en cualquier momento.
La sintaxis es:
let {proxy, revoke} = Proxy.revocable(target, handler)
La llamada devuelve un objeto con el proxy y la función revoke para deshabilitarlo.
Aquà hay un ejemplo:
let object = {
data: "datos valiosos"
};
let {proxy, revoke} = Proxy.revocable(object, {});
// pasamos el proxy en lugar del objeto...
alert(proxy.data); // datos valiosos
// luego en nuestro código
revoke();
// el proxy no funciona más (revocado)
alert(proxy.data); // Error
La llamada a revoke() quita al proxy todas las referencias internas hacia el objeto target, ya no estarán conectados.
En principio revoke está separado de proxy, asà que podemos pasar proxy alrededor mientras mantenemos revoke en la vista actual.
También podemos vincular el método revoke al proxy asignándolo como propiedad: proxy.revoke = revoke.
Otra opción es crear un WeakMap que tenga a proxy como clave y su correspondiente revoke como valor, esto permite fácilmente encontrar el revoke para un proxy:
let revokes = new WeakMap();
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// ...en algún otro lado de nuestro código...
revoke = revokes.get(proxy);
revoke();
alert(proxy.data); // Error (revocado)
Usamos WeakMap en lugar de Map aquà porque no bloqueará la recolección de basura. Si el objeto proxy se vuelve inalcanzable (es decir, ya ninguna variable hace referencia a él), WeakMap permite eliminarlo junto con su revoke que no necesitaremos más.
References
Resumen
Proxy es un envoltorio (wrapper) alrededor de un objeto que redirige las operaciones en el hacia el objeto, opcionalmente atrapando algunas de ellas para manejarlas por su cuenta.
Puede envolver cualquier tipo de objeto, incluyendo clases y funciones.
La sintaxis es:
let proxy = new Proxy(target, {
/* trampas */
});
â¦Entonces deberÃamos usar proxy en todos lados en lugar de target. Un proxy no tiene sus propias propiedades o métodos. Atrapa una operación si la trampa correspondiente le es provista, de otro modo la reenvÃa al objeto target.
Podemos atrapar:
- Lectura (
get), escritura (set), eliminación de propiedad (deleteProperty) (incluso si no existe). - Llamadas a función (trampa
apply). - El operador
new(trampaconstruct). - Muchas otras operaciones (la lista completa al principio del artÃculo y en docs).
Esto nos permite crear propiedades y métodos âvirtualesâ, implementar valores por defecto, objetos observables, decoradores de función y mucho más.
También podemos atrapar un objeto múltiples veces en proxys diferentes, decorándolos con varios aspectos de funcionalidad.
La API de Reflect está diseñada para complementar Proxy. Para cada trampa de Proxy hay una llamada Reflect con los mismos argumentos. DeberÃamos usarlas para redirigir llamadas hacia los objetos target.
Los proxys tienen algunas limitaciones:
- Los objetos nativos tienen âslots internosâ a los que el proxy no tiene acceso. Ver la forma de sortear el problema más arriba.
- Lo mismo cuenta para los campos privados en las clases porque están implementados internamente usando slots. Entonces las llamadas a métodos atrapados deben tener en
thisal objeto target para poder accederlos. - El test de igualdad de objeto
===no puede ser interceptado. - Performance: los tests de velocidad dependen del motor, pero generalmente acceder a una propiedad usando el proxy más simple el tiempo se multiplica unas veces. Aunque en la práctica esto solo es importante para los objetos que son los âcuello de botellaâ de una aplicación.
Comentarios
<code>, para varias lÃneas â envolverlas en la etiqueta<pre>, para más de 10 lÃneas â utilice una entorno controlado (sandbox) (plnkr, jsbin, codepenâ¦)