Kodokon kodokon.com

Proxy, Reflect y metaprogramación

Intercepta las operaciones fundamentales del lenguaje con las trampas de Proxy y delega limpiamente el comportamiento por defecto con Reflect.

10 min · 3 preguntas

Abrir esta lección en Kodokon

Un Proxy envuelve un objeto objetivo e intercepta las operaciones internas del lenguaje mediante trece trampas: get, set, has (el operador in), deleteProperty, ownKeys, apply, construct, etc. El objeto Reflect expone exactamente las mismas operaciones como funciones, con las mismas firmas que las trampas. Es la base de la reactividad de Vue 3, de los mocks dinámicos y de las API al estilo ORM.

JAVASCRIPT
const target = { name: "Ada" };

const traced = new Proxy(target, {
  get(obj, prop, receiver) {
    console.log("get " + String(prop));
    return Reflect.get(obj, prop, receiver);
  }
});

traced.name; // get name
Una trampa get mínima delegada a Reflect.

¿Por qué Reflect.get(obj, prop, receiver) en lugar de obj[prop]? Por el receiver. Si la propiedad es un descriptor de acceso (un getter), Reflect.get lo invoca con this apuntando al receiver - el proxy o un objeto que hereda de él - mientras que obj[prop] establecería this en el objetivo en bruto. Sin el receiver, los getters que leen this se rompen en cuanto un objeto hereda del proxy.

JAVASCRIPT
const base = {
  get label() {
    return this.name;
  }
};

const proxy = new Proxy(base, {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);
  }
});

const child = Object.create(proxy);
child.name = "Ada";
console.log(child.label); // "Ada" thanks to the receiver
El receiver preserva this en los getters heredados.

Los casos de uso serios: validación al escribir, propiedades virtuales calculadas al vuelo, registro, control de acceso (ocultar claves con prefijo _ mediante has y ownKeys), u objetos con comportamiento por defecto. Ten en cuenta que Reflect.set devuelve un booleano en lugar de lanzar en modo estricto: tu trampa debe propagar ese valor de retorno.

JAVASCRIPT
function validated(target) {
  return new Proxy(target, {
    set(obj, prop, value, receiver) {
      if (prop === "age" && !Number.isInteger(value)) {
        throw new TypeError("age must be an integer");
      }
      return Reflect.set(obj, prop, value, receiver);
    }
  });
}

const user = validated({ age: 30 });
user.age = 31;       // ok
// user.age = "old"; // TypeError
Validación transparente al escribir.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. En una trampa get, ¿por qué preferir Reflect.get(target, prop, receiver) en lugar de target[prop]?
    • Es más rápido porque Reflect evita el prototipo
    • Para que los getters se ejecuten con el this correcto (el receiver) en lugar del objetivo en bruto
    • Porque target[prop] volvería a activar la trampa y causaría recursión infinita
    • Las dos formas son estrictamente equivalentes
  2. ¿Qué trampa intercepta la expresión "id" in obj?
    • get
    • has
    • ownKeys
    • getOwnPropertyDescriptor
  3. Una trampa get devuelve 42 para una propiedad del objetivo cuyo valor es 1, declarada no configurable y no escribible. ¿Qué ocurre?
    • El proxy devuelve 42, la trampa siempre tiene prioridad
    • El motor devuelve 1 en silencio
    • El motor lanza un TypeError por violación de invariante
    • El comportamiento depende del motor de JavaScript