Proxy y Reflect en JavaScript: traps, metaprogramación y patrones prácticos

Proxy y Reflect son dos APIs de metaprogramación introducidas en ES2015 que permiten interceptar y redefinir operaciones fundamentales sobre objetos: leer propiedades, escribirlas, invocar funciones, construir instancias y más. Son la base de sistemas de reactividad como Vue 3 y de herramientas de validación automática.

Proxy: interceptar operaciones con traps

Un Proxy envuelve un objeto objetivo (target) y un manejador (handler) con métodos llamados traps. Cada trap intercepta una operación específica: get para leer propiedades, set para escribirlas, has para el operador in, deleteProperty para delete, etc.

const handler = {
  get(target, prop) {
    console.log(`Leyendo: ${prop}`);
    return prop in target ? target[prop] : `[${prop} no existe]`;
  },
  set(target, prop, valor) {
    console.log(`Escribiendo: ${prop} = ${valor}`);
    target[prop] = valor;
    return true; // obligatorio en set
  },
};

const obj = new Proxy({}, handler);
obj.nombre = 'Ana';       // Escribiendo: nombre = Ana
console.log(obj.nombre);  // Leyendo: nombre ? "Ana"
console.log(obj.edad);    // Leyendo: edad ? "[edad no existe]"

Validación de propiedades con Proxy

El trap set permite imponer restricciones sobre qué valores son aceptables, centralizando la lógica de validación sin necesidad de setters individuales:

function crearPersona(datos) {
  return new Proxy(datos, {
    set(target, prop, valor) {
      if (prop === 'edad') {
        if (typeof valor !== 'number' || valor < 0 || valor > 150) {
          throw new RangeError(`Edad inválida: ${valor}`);
        }
      }
      if (prop === 'email') {
        if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(valor)) {
          throw new TypeError(`Email inválido: ${valor}`);
        }
      }
      target[prop] = valor;
      return true;
    },
  });
}

const persona = crearPersona({ nombre: 'Juan' });
persona.edad = 30;           // OK
persona.email = '[email protected]'; // OK
persona.edad = -5;           // RangeError: Edad inválida: -5

Objetos observables tipo Vue

Vue 3 usa Proxy internamente para detectar cambios en el estado reactivo. Esta es una implementación simplificada que notifica a los suscriptores cuando cambia una propiedad:

function observable(datos) {
  const oyentes = new Map();

  const proxy = new Proxy(datos, {
    set(target, prop, valor) {
      const anterior = target[prop];
      target[prop] = valor;
      if (anterior !== valor && oyentes.has(prop)) {
        oyentes.get(prop).forEach(fn => fn(valor, anterior));
      }
      return true;
    },
  });

  proxy.$watch = (prop, fn) => {
    if (!oyentes.has(prop)) oyentes.set(prop, new Set());
    oyentes.get(prop).add(fn);
    return () => oyentes.get(prop).delete(fn); // función de limpieza
  };

  return proxy;
}

const estado = observable({ contador: 0 });
const limpiar = estado.$watch('contador', (nuevo, viejo) =>
  console.log(`contador: ${viejo} ? ${nuevo}`)
);

estado.contador = 1; // contador: 0 ? 1
estado.contador = 5; // contador: 1 ? 5
limpiar();           // elimina el observador

Conversión automática snake_case a camelCase

Un Proxy puede transformar el acceso a propiedades de forma transparente, por ejemplo para trabajar con respuestas de API en snake_case desde código JavaScript en camelCase:

const snakeToCamel = str =>
  str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());

function adaptarAPI(datos) {
  return new Proxy(datos, {
    get(target, prop) {
      if (prop in target) return target[prop];
      const snake = prop.replace(/([A-Z])/g, '_$1').toLowerCase();
      return target[snake];
    },
  });
}

const resp = adaptarAPI({ user_name: 'Ana', created_at: '2024-01-01' });
console.log(resp.userName);  // "Ana"
console.log(resp.createdAt); // "2024-01-01"

Reflect: el complemento natural de Proxy

Reflect expone las operaciones primitivas de los objetos como funciones, con la misma firma que los traps de Proxy. Úsalo dentro de los traps para invocar el comportamiento por defecto sin romper la cadena de prototipos:

function memoizar(fn) {
  const cache = new Map();

  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const clave = JSON.stringify(args);
      if (cache.has(clave)) {
        console.log(`Cache hit: ${clave}`);
        return cache.get(clave);
      }
      const resultado = Reflect.apply(target, thisArg, args);
      cache.set(clave, resultado);
      return resultado;
    },
  });
}

const suma = memoizar((a, b) => a + b);
suma(2, 3); // calcula: 5
suma(2, 3); // Cache hit: [2,3] ? 5

Usar Reflect.apply en lugar de target.apply garantiza que el trap funciona correctamente incluso cuando el objetivo no es una función normal, respetando el receptor (this) de forma segura.

COMPARTE ESTE ARTÍCULO

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