Proxy y Reflect en JavaScript: interceptar operaciones sobre objetos

Proxy y Reflect son dos API de metaprogramación introducidas en ES6 que permiten interceptar y personalizar operaciones fundamentales sobre objetos: acceso a propiedades, asignación, llamadas a funciones, in, delete, y más. Son las herramientas que usan internamente frameworks como Vue 3 para implementar reactividad.

Proxy: interceptar operaciones con handlers

Un Proxy envuelve un objeto objetivo y permite interceptar operaciones a través de un objeto handler que define traps (trampas). Cada trap corresponde a una operación del lenguaje:

const objetivo = { nombre: 'Ana', edad: 30 };

const proxy = new Proxy(objetivo, {
  // get: se activa al leer una propiedad
  get(target, propiedad, receiver) {
    console.log(`Leyendo: ${propiedad}`);
    return Reflect.get(target, propiedad, receiver);
  },
  // set: se activa al asignar una propiedad
  set(target, propiedad, valor, receiver) {
    console.log(`Asignando ${propiedad} = ${valor}`);
    return Reflect.set(target, propiedad, valor, receiver);
  }
});

proxy.nombre;       // "Leyendo: nombre"
proxy.edad = 31;    // "Asignando edad = 31"

Validación automática con Proxy

Una de las aplicaciones más útiles: validar datos antes de asignarlos, lanzando errores descriptivos en lugar de aceptar datos inválidos silenciosamente:

const esquema = {
  nombre: { tipo: 'string', requerido: true },
  edad: { tipo: 'number', min: 0, max: 120 },
  email: { tipo: 'string' }
};

function crearValidado(datos, esquema) {
  return new Proxy(datos, {
    set(target, prop, valor) {
      if (!(prop in esquema)) {
        throw new TypeError(`Propiedad desconocida: ${prop}`);
      }
      const regla = esquema[prop];
      if (typeof valor !== regla.tipo) {
        throw new TypeError(`${prop} debe ser ${regla.tipo}`);
      }
      if (regla.min !== undefined && valor < regla.min) {
        throw new RangeError(`${prop} debe ser >= ${regla.min}`);
      }
      if (regla.max !== undefined && valor > regla.max) {
        throw new RangeError(`${prop} debe ser <= ${regla.max}`);
      }
      return Reflect.set(target, prop, valor);
    }
  });
}

const usuario = crearValidado({}, esquema);
usuario.nombre = 'Ana';   // OK
usuario.edad = 25;         // OK
// usuario.edad = -5;      // RangeError: edad debe ser >= 0
// usuario.activo = true;  // TypeError: Propiedad desconocida: activo

Valores por defecto con get trap

Puedes interceptar lecturas para devolver valores por defecto en lugar de undefined:

function conDefecto(obj, valorPorDefecto = 0) {
  return new Proxy(obj, {
    get(target, propiedad) {
      return propiedad in target
        ? target[propiedad]
        : valorPorDefecto;
    }
  });
}

const contadores = conDefecto({}, 0);
contadores.visitas++;  // 0 + 1 = 1 (no undefined + 1 = NaN)
contadores.clicks++;
contadores.visitas++;

console.log(contadores.visitas);      // 2
console.log(contadores.clicks);       // 1
console.log(contadores.inexistente);  // 0

Objetos reactivos (como Vue 3)

Proxy es la base de la reactividad en Vue 3: detecta cuándo se leen y escriben propiedades para actualizar la UI automáticamente:

function reactivo(datos) {
  const suscriptores = new Map();

  return new Proxy(datos, {
    get(target, propiedad) {
      // Registrar quién lee esta propiedad (en Vue: efecto actual)
      if (!suscriptores.has(propiedad)) {
        suscriptores.set(propiedad, new Set());
      }
      console.log(`[reactive] get: ${propiedad}`);
      return Reflect.get(target, propiedad);
    },
    set(target, propiedad, valor) {
      const anterior = target[propiedad];
      const resultado = Reflect.set(target, propiedad, valor);
      if (anterior !== valor) {
        // Notificar a todos los suscriptores (en Vue: re-renderizar)
        console.log(`[reactive] ${propiedad} cambió: ${anterior} ? ${valor}`);
        suscriptores.get(propiedad)?.forEach(fn => fn(valor, anterior));
      }
      return resultado;
    }
  });
}

const estado = reactivo({ contador: 0, nombre: 'Ana' });
estado.contador = 1;  // [reactive] contador cambió: 0 ? 1
estado.nombre;        // [reactive] get: nombre

Reflect: el espejo de las operaciones de objeto

Reflect proporciona métodos estáticos que corresponden a las traps de Proxy. Siempre deberías usar Reflect dentro de los handlers en lugar de operar directamente sobre el target, para preservar el comportamiento correcto del prototipo:

const obj = { x: 1, y: 2 };

// Operaciones de Reflect (equivalentes a los operadores del lenguaje)
Reflect.get(obj, 'x');            // 1   (equivale a obj.x)
Reflect.set(obj, 'z', 3);        // true (equivale a obj.z = 3)
Reflect.has(obj, 'x');           // true (equivale a 'x' in obj)
Reflect.deleteProperty(obj, 'y'); // true (equivale a delete obj.y)
Reflect.ownKeys(obj);            // ['x', 'z']

// En un handler de Proxy, Reflect asegura el comportamiento correcto:
const proxy = new Proxy(obj, {
  get(target, prop, receiver) {
    // receiver es el proxy (importante para getters heredados)
    return Reflect.get(target, prop, receiver);  // CORRECTO
    // return target[prop];  // Puede fallar con getters y prototipos
  }
});

Proxy no es adecuado para todo: las trampas añaden coste en cada acceso, y algunos objetos nativos (como Date o Map) no se pueden proxiar de forma transparente sin trampas adicionales. Úsalo cuando el valor de la interceptación justifica la complejidad añadida.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP