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.
