Metaprogramación con Proxy y Reflect

Avanzado
JavaScript
JavaScript
Actualizado: 04/05/2026

Diagrama: Proxy y Reflect en JavaScript

Qué es un Proxy

Un Proxy es un objeto que envuelve a otro (el target) y permite interceptar las operaciones fundamentales que se hacen sobre él: leer una propiedad, asignar, comprobar si existe, eliminar, enumerar claves, etc. Se construye con dos argumentos: el objeto real y un handler con las funciones trap que queremos personalizar.

const target = { name: "Alan", age: 34 };

const proxy = new Proxy(target, {
  get(obj, prop, receiver) {
    console.log(`GET ${String(prop)}`);
    return obj[prop];
  }
});

console.log(proxy.name); // "GET name" — "Alan"
console.log(proxy.age);  // "GET age" — 34

Si un trap no está definido, la operación se delega al comportamiento por defecto sobre el target. Esta es la idea clave: un Proxy mínimo es transparente; solo altera aquello que decidimos interceptar.

Traps más habituales

El handler admite una docena de traps, una por cada operación del lenguaje. Los más frecuentes son get, set, has y deleteProperty.

| Trap | Intercepta | Equivalente en Reflect | |------|-----------|------------------------| | get(target, prop, receiver) | proxy.prop | Reflect.get(...) | | set(target, prop, value, receiver) | proxy.prop = value | Reflect.set(...) | | has(target, prop) | prop in proxy | Reflect.has(...) | | deleteProperty(target, prop) | delete proxy.prop | Reflect.deleteProperty(...) | | ownKeys(target) | Object.keys, for...in | Reflect.ownKeys(...) | | apply(target, thisArg, args) | Llamada a una función | Reflect.apply(...) | | construct(target, args, newTarget) | new proxy(...) | Reflect.construct(...) |

Reflect: la contraparte explícita

Reflect no es un constructor; es un objeto con métodos estáticos que reflejan cada operación fundamental del lenguaje. Son los mismos nombres y argumentos que los traps, lo que los convierte en el complemento natural:

  • Reflect devuelve valores consistentes: Reflect.set y Reflect.deleteProperty devuelven true/false en lugar de lanzar en modos distintos.
  • Reflect permite implementar el comportamiento por defecto de un trap sin escribirlo a mano.
  • Reflect respeta el receiver correcto en getters y setters, cosa que un simple target[prop] podría no hacer.

Un trap típico casi siempre termina delegando en Reflect:

const handler = {
  get(target, prop, receiver) {
    console.log(`GET ${String(prop)}`);
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log(`SET ${String(prop)} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
};

Sin Reflect tendríamos que escribir target[prop] y target[prop] = value, con peor manejo del this en getters/setters y sin valor de retorno consistente para set.

Casos prácticos

Validación al asignar

Un Proxy puede garantizar que no se escriben valores inválidos en un objeto. Centralizamos la validación en el trap set:

function createUser() {
  const target = { name: "", age: 0 };

  return new Proxy(target, {
    set(obj, prop, value, receiver) {
      if (prop === "age" && (typeof value !== "number" || value < 0)) {
        throw new TypeError(`age debe ser un número no negativo`);
      }
      if (prop === "name" && typeof value !== "string") {
        throw new TypeError(`name debe ser una cadena`);
      }
      return Reflect.set(obj, prop, value, receiver);
    }
  });
}

const user = createUser();
user.name = "Alan";       // OK
user.age = 34;            // OK
// user.age = -1;         // TypeError

Logging y auditoría

El patrón del Proxy es perfecto para registrar qué operaciones se hacen sobre un objeto, útil en depuración o auditoría:

function audit(obj, label = "obj") {
  return new Proxy(obj, {
    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver);
      console.log(`[${label}] get ${String(prop)}`);
      return value;
    },
    set(target, prop, value, receiver) {
      console.log(`[${label}] set ${String(prop)} = ${JSON.stringify(value)}`);
      return Reflect.set(target, prop, value, receiver);
    },
    deleteProperty(target, prop) {
      console.log(`[${label}] delete ${String(prop)}`);
      return Reflect.deleteProperty(target, prop);
    }
  });
}

const config = audit({ env: "dev", debug: true }, "config");
config.env = "prod";     // [config] set env = "prod"
delete config.debug;     // [config] delete debug

Claves "privadas" con prefijo _

Con get, has y ownKeys podemos ocultar propiedades que empiecen por _, emulando privacidad desde el exterior:

function hidePrivate(obj) {
  const isPrivate = prop => typeof prop === "string" && prop.startsWith("_");

  return new Proxy(obj, {
    get(target, prop, receiver) {
      if (isPrivate(prop)) return undefined;
      return Reflect.get(target, prop, receiver);
    },
    has(target, prop) {
      if (isPrivate(prop)) return false;
      return Reflect.has(target, prop);
    },
    ownKeys(target) {
      return Reflect.ownKeys(target).filter(k => !isPrivate(k));
    },
    getOwnPropertyDescriptor(target, prop) {
      if (isPrivate(prop)) return undefined;
      return Reflect.getOwnPropertyDescriptor(target, prop);
    }
  });
}

const api = hidePrivate({ name: "Alan", _password: "secreto" });
console.log(api.name);                 // "Alan"
console.log(api._password);            // undefined
console.log("_password" in api);       // false
console.log(Object.keys(api));         // ["name"]

Conviene recordar que es una ocultación "externa": el objeto subyacente sigue teniendo esas propiedades.

Lazy loading de propiedades

Un Proxy puede resolver propiedades solo cuando se piden, evitando cargar datos caros hasta que sean necesarios:

function lazyLoader(loader) {
  const cache = new Map();
  return new Proxy({}, {
    get(_, prop) {
      if (!cache.has(prop)) {
        cache.set(prop, loader(prop));
      }
      return cache.get(prop);
    }
  });
}

const translations = lazyLoader(key => {
  console.log(`Cargando traducción de '${key}'`);
  return `(traducción de ${key})`;
});

translations.hello;   // Cargando traducción de 'hello'
translations.hello;   // cache hit, sin log
translations.welcome; // Cargando traducción de 'welcome'

Valores por defecto

Con get podemos devolver un valor genérico para propiedades no definidas, imitando el defaultdict de Python:

const counters = new Proxy({}, {
  get(target, prop) {
    return prop in target ? target[prop] : 0;
  }
});

counters.a = counters.a + 1;  // era 0, ahora 1
counters.a = counters.a + 1;  // 2
console.log(counters.a, counters.b); // 2 0

Observación con revoke

Proxy.revocable(target, handler) produce un { proxy, revoke }. Al llamar a revoke(), cualquier operación posterior sobre el proxy lanza un TypeError. Es útil para revocar acceso a un objeto entregado temporalmente:

const { proxy, revoke } = Proxy.revocable({ secret: 42 }, {});

console.log(proxy.secret); // 42
revoke();
// console.log(proxy.secret); // TypeError: revoked

Interceptar llamadas y constructores

Los traps apply y construct permiten envolver funciones o clases, no solo objetos planos.

function sum(a, b) { return a + b; }

const logged = new Proxy(sum, {
  apply(target, thisArg, args) {
    console.log(`llamando sum(${args.join(", ")})`);
    return Reflect.apply(target, thisArg, args);
  }
});

logged(3, 4); // "llamando sum(3, 4)" — devuelve 7
class User { constructor(name) { this.name = name; } }

const LoggedUser = new Proxy(User, {
  construct(target, args, newTarget) {
    console.log(`new User(${args.join(", ")})`);
    return Reflect.construct(target, args, newTarget);
  }
});

new LoggedUser("Alan"); // "new User(Alan)"

Limitaciones y coste

  • Identidad: un Proxy es un objeto distinto del target. Comparaciones por referencia o uso como clave en Map/WeakMap pueden no coincidir si mezclamos proxy y original.
  • Métodos internos: no todos los mecanismos del lenguaje pasan por las operaciones interceptables. Por ejemplo, los private fields (#campo) no son visibles a través de un Proxy del objeto que los contiene.
  • Rendimiento: cada acceso pasa por el handler. Para rutas calientes con millones de accesos, el coste es relevante. Mide antes de usar Proxy en rutas críticas.
  • Invariantes: algunos traps deben cumplir invariantes (p. ej., si una propiedad es no configurable y no escribible en el target, get debe devolver el mismo valor). Violarlos provoca TypeError.

Buenas prácticas

  • Usa Proxy cuando necesites interceptar operaciones que de verdad van a ocurrir: validación en un editor, auditoría, lazy loading, APIs genéricas. Si simplemente quieres un getter/setter por campo, usa Object.defineProperty o class con getters.
  • Delega siempre que puedas en Reflect.xxx dentro del trap: así respetas el comportamiento por defecto y los receiver correctos.
  • Mantén los handlers pequeños; no metas lógica de negocio dentro de los traps. Usa el trap para decidir y llama a funciones bien nombradas para el trabajo real.
  • Si el proxy tiene una vida limitada, considera Proxy.revocable para poder cerrar el acceso cuando ya no sea válido.
  • Documenta las claves que tu proxy trata de forma especial; desde fuera no se ve.
  • No uses Proxy para enmascarar un diseño confuso. A veces una clase con métodos claros es más mantenible que un Proxy con 6 traps.

Resumen

Proxy y Reflect convierten JavaScript en un lenguaje con capacidades de metaprogramación de primer nivel: podemos observar, validar o modificar prácticamente cualquier operación fundamental sobre un objeto. Reflect simplifica los traps al ofrecer equivalentes explícitos y consistentes a cada operación. Usados con mesura, permiten resolver elegantemente problemas como la validación declarativa, el registro de accesos, el lazy loading o la ocultación selectiva de propiedades. Como contrapartida añaden un coste en rendimiento y en complejidad, así que conviene reservarlos para los casos donde realmente aportan claridad.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, JavaScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de JavaScript

Explora más contenido relacionado con JavaScript y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Entender qué operaciones fundamentales del lenguaje se pueden interceptar con Proxy.
  • Definir traps (get, set, has, deleteProperty, ownKeys...).
  • Usar Reflect como contraparte directa de cada trap para delegar al comportamiento por defecto.
  • Implementar casos prácticos: validación, logging, lazy loading, ocultar claves internas.
  • Conocer los límites: métodos privados, identidad, rendimiento.
  • Diferenciar cuándo usar Proxy y cuándo otra técnica más sencilla.