Metaprogramación

A partir de ECMAScript 2015, JavaScript gana soporte para los objetos Proxy y Reflect lo cual te permite interceptar y definir un comportamiento personalizado para las operaciones fundamentales del lenguaje (por ejemplo, búsqueda de propiedades, asignación, enumeración, invocación de funciones, etc.). Con la ayuda de estos dos objetos, puedes programar en el metanivel de JavaScript.

Proxies

Introducidos en ECMAScript 6, los objetos Proxy te permiten interceptar ciertas operaciones e implementar comportamientos personalizados.

Por ejemplo, obtener una propiedad sobre un objeto:

js
let handler = {
  get: function (target, name) {
    return name in target ? target[name] : 42;
  },
};

let p = new Proxy({}, handler);
p.a = 1;
console.log(p.a, p.b); // 1, 42

El objeto Proxy define un target (un objeto vacío aquí) y un objeto handler, en el que se implementa un get trap. Aquí, un objeto que es proxy no devolverá undefined cuando obtenga propiedades indefinidas, sino que devolverá el número 42.

Hay ejemplos adicionales disponibles en la página de referencia Proxy.

Terminología

Los siguientes términos se utilizan cuando se habla de la funcionalidad de los proxies.

handler

Objeto marcador de posición que contiene trampas.

traps

Los métodos que proporcionan acceso a la propiedad. (Esto es análogo al concepto de trampas en los sistemas operativos).

target

Objeto que virtualiza el proxy. A menudo se utiliza como interfaz de administración de almacenamiento para el proxy. Las invariantes (semántica que permanece sin cambios) con respecto a la no extensibilidad del objeto o las propiedades no configurables se verifican con el target.

invariants

La semántica que permanece sin cambios al implementar operaciones personalizadas se denominan invariants. Si violas las invariantes de un controlador, se lanzará un TypeError.

Controladores y trampas

La siguiente tabla resume las trampas disponibles para los objetos Proxy. Ve las páginas de referencia para explicaciones detalladas y ejemplos.

Controlador/Trampa Intercepciones Invariantes
handler.getPrototypeOf() Object.getPrototypeOf()
Reflect.getPrototypeOf()
__proto__
Object.prototype.isPrototypeOf()
instanceof
  • El método getPrototypeOf regresa un objeto o null.
  • Si target no es extensible, el método Object.getPrototypeOf(proxy) debe devolver el mismo valor que Object.getPrototypeOf(target).
handler.setPrototypeOf() Object.setPrototypeOf()
Reflect.setPrototypeOf()
Si target no es extensible, el parámetro prototype debe tener el mismo valor que Object.getPrototypeOf(target).
handler.isExtensible() Object.isExtensible()
Reflect.isExtensible()
Object.isExtensible(proxy) debe devolver el mismo valor que Object.isExtensible(target).
handler.preventExtensions() Object.preventExtensions()
Reflect.preventExtensions()
Object.preventExtensions(proxy) solo devuelve true si Object.isExtensible(proxy) es false.
handler.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor()
Reflect.getOwnPropertyDescriptor()
  • getOwnPropertyDescriptor debe devolver un objeto o undefined.
  • Una propiedad no se puede reportar como inexistente si existe como una propiedad propia no configurable de target.
  • Una propiedad no se puede reportar como inexistente si existe como propiedad propia de target y target no es extensible.
  • Una propiedad no se puede reportar como existente si no existe como una propiedad propia de target y target no es extensible.
  • No se puede reportar una propiedad como no configurable si no existe como propiedad propia de target o si existe como propiedad propia configurable de target.
  • El resultado de Object.getOwnPropertyDescriptor(target) se puede aplicar a target usando Object.defineProperty y no lanzará una excepción.
handler.defineProperty() Object.defineProperty()
Reflect.defineProperty()
  • No se puede agregar una propiedad si target no es extensible.
  • Una propiedad no se puede agregar como (o modificar para ser) no configurable si no existe como una propiedad propia no configurable de target.
  • Una propiedad no puede ser no configurable si existe una propiedad configurable correspondiente de target.
  • Si una propiedad tiene una propiedad de objeto target correspondiente, entonces Object.defineProperty(target, prop, descriptor) no lanzará una excepción.
  • En modo estricto, un valor false devuelto por el controlador defineProperty lanzará una excepción TypeError.
handler.has()
Consulta de propiedad
foo in proxy
Consulta de propiedad heredada
foo in Object.create(proxy)
Reflect.has()
  • Una propiedad no se puede reportar como inexistente, si existe como una propiedad propia no configurable de target.
  • Una propiedad no se puede reportar como inexistente si existe como propiedad propia de target y target no es extensible.
handler.get()
Acceso a la propiedad
proxy[foo]
proxy.bar
Acceso a propiedad heredada
Object.create[proxy](foo)
Reflect.get()
  • El valor reportado para una propiedad debe ser el mismo que el valor de la propiedad target correspondiente si la propiedad de target es una propiedad de datos de solo lectura y no es configurable.
  • El valor reportado para una propiedad debe ser undefined si la propiedad target correspondiente es una propiedad de acceso no configurable que tiene undefined como su atributo [[Get]].
handler.set()
Asignación de propiedad
proxy[foo] = bar
proxy.foo = bar
Asignación de propiedad heredada
Object.create[proxy](foo) = bar
{jsxref("Reflect.set()")}}
  • No se puede cambiar el valor de una propiedad para que sea diferente del valor de la propiedad target correspondiente si la propiedad target correspondiente es una propiedad de datos de solo lectura y no es configurable.
  • No se puede establecer el valor de una propiedad si la propiedad target correspondiente es una propiedad de acceso no configurable que tiene undefined como su atributo [[Get]].
  • En modo estricto, un valor de retorno false del controlador set arrojará una excepción TypeError.
handler.deleteProperty()
Eliminación de propiedad
delete proxy[foo]
delete proxy.foo
Reflect.deleteProperty()
Una propiedad no se puede eliminar si existe como una propiedad propia no configurable de target.
handler.enumerate()
Enumeración de propiedad/for...in:
for (let name in proxy) {...}
Reflect.enumerate()
El método enumerate debe devolver un objeto.
handler.ownKeys() Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
Reflect.ownKeys()
  • El resultado de ownKeys es una lista.
  • El Tipo de cada elemento de la Lista de resultados es String o Symbol.
  • La Lista de resultados debe contener las claves de todas las propiedades propias no configurables de target.
  • Si el objeto target no es extensible, entonces la Lista de resultados debe contener todas las claves de las propiedades propias de target y ningún otro valor.
handler.apply() proxy(..args)
Function.prototype.apply() y Function.prototype.call()
Reflect.apply()
No hay invariantes para el método handler.apply.
handler.construct() new proxy(...args)
Reflect.construct()
El resultado debe ser un Objeto.

Proxy revocable

El método Proxy.revocable() se usa para crear un objeto Proxy revocable. Esto significa que el proxy se puede revocar mediante la función revoke y apagar el proxy.

Posteriormente, cualquier operación en el proxy conduce a un TypeError.

js
let revocable = Proxy.revocable(
  {},
  {
    get: function (target, name) {
      return "[[" + name + "]]";
    },
  },
);
let proxy = revocable.proxy;
console.log(proxy.foo); // "[[foo]]"

revocable.revoke();

console.log(proxy.foo); // Lanza TypeError
proxy.foo = 1; // TypeError nuevamente
delete proxy.foo; // todavía TypeError
typeof proxy; // "object", typeof no activa ninguna trampa

Reflexión

Reflect es un objeto integrado que proporciona métodos para operaciones JavaScript interceptables. Los métodos son los mismos que los de proxy handlers.

Reflect no es un objeto función.

Reflect ayuda con el reenvío de las operaciones predeterminadas del controlador al target.

Con Reflect.has() por ejemplo, obtienes el operador in como función:

js
Reflect.has(Object, "assign"); // true

Una mejor función apply

En ES5, normalmente usas el método Function.prototype.apply() para llamar a una función con un valor this y arguments proporcionado como un arreglo (o un objeto similar a un arreglo).

js
Function.prototype.apply.call(Math.floor, undefined, [1.75]);

Con Reflect.apply esto se vuelve menos detallado y más fácil de entender:

js
Reflect.apply(Math.floor, undefined, [1.75]);
// 1;

Reflect.apply(String.fromCharCode, undefined, [104, 101, 108, 108, 111]);
// "hola"

Reflect.apply(RegExp.prototype.exec, /ab/, ["confabulation"]).index;
// 4

Reflect.apply("".charAt, "ponies", [3]);
// "i"

Comprobando si la definición de la propiedad ha sido exitosa

Con Object.defineProperty, que devuelve un objeto si tiene éxito, o arroja un TypeError de lo contrario, usaría un bloque try...catch para detectar cualquier error que haya ocurrido al definir una propiedad. Debido a que Reflect.defineProperty devuelve un estado de éxito booleano, aquí puedes usar un bloque if...else:

js
if (Reflect.defineProperty(target, property, attributes)) {
  // éxito
} else {
  // fracaso
}