TechLead

Proxy y Reflect

Metaprogramación con objetos Proxy y la API Reflect

Introducción a la Metaprogramación

La Metaprogramación es escribir código que manipula o intercepta otro código en tiempo de ejecución. Las APIs Proxy y Reflect de JavaScript permiten patrones poderosos de metaprogramación como validación, logging, objetos virtuales y sistemas reactivos.

Conceptos Clave

  • Proxy — Envuelve un objeto para interceptar y personalizar operaciones
  • Handler — Objeto que contiene métodos trampa
  • Trampa (Trap) — Método que intercepta una operación (get, set, etc.)
  • Reflect — API que refleja trampas de Proxy con comportamientos predeterminados

Proxy Básico

// Sintaxis: new Proxy(objetivo, manejador)
const objetivo = {
  nombre: "Alice",
  edad: 30
};

const manejador = {
  get(objetivo, propiedad, receptor) {
    console.log(`Obteniendo ${propiedad}`);
    return objetivo[propiedad];
  },
  
  set(objetivo, propiedad, valor, receptor) {
    console.log(`Estableciendo ${propiedad} a ${valor}`);
    objetivo[propiedad] = valor;
    return true; // Debe retornar true para éxito
  }
};

const proxy = new Proxy(objetivo, manejador);

proxy.nombre;        // Registra: "Obteniendo nombre", retorna "Alice"
proxy.edad = 31;     // Registra: "Estableciendo edad a 31"
console.log(proxy.edad); // 31

Trampas Comunes de Proxy

Trampa Intercepta
get(target, prop) Acceso a propiedad: obj.prop
set(target, prop, value) Asignación de propiedad: obj.prop = valor
has(target, prop) Operador in
deleteProperty(target, prop) Operador delete
apply(target, thisArg, args) Llamadas a funciones
construct(target, args) Operador new

Proxy de Validación

function crearObjetoValidado(esquema) {
  return new Proxy({}, {
    set(objetivo, propiedad, valor) {
      const validador = esquema[propiedad];
      
      if (!validador) {
        throw new Error(`Propiedad desconocida: ${propiedad}`);
      }
      
      if (!validador(valor)) {
        throw new Error(`Valor inválido para ${propiedad}: ${valor}`);
      }
      
      objetivo[propiedad] = valor;
      return true;
    }
  });
}

const usuario = crearObjetoValidado({
  nombre: (v) => typeof v === "string" && v.length > 0,
  edad: (v) => typeof v === "number" && v >= 0 && v < 150,
  email: (v) => /^[^@]+@[^@]+\.[^@]+$/.test(v)
});

usuario.nombre = "Alice";    // ✓ OK
usuario.edad = 30;           // ✓ OK
usuario.edad = -5;           // ✗ Error: Valor inválido para edad
usuario.foo = "bar";         // ✗ Error: Propiedad desconocida: foo

Valores Predeterminados y Propiedades Virtuales

// Crear automáticamente propiedades faltantes
const conDefectos = new Proxy({}, {
  get(objetivo, propiedad) {
    if (!(propiedad in objetivo)) {
      objetivo[propiedad] = 0; // Valor predeterminado
    }
    return objetivo[propiedad];
  }
});

console.log(conDefectos.cuenta); // 0 (auto-creado)
conDefectos.cuenta++;
console.log(conDefectos.cuenta); // 1

// Propiedades virtuales (computadas)
const usuario = {
  nombre: "Juan",
  apellido: "Pérez"
};

const proxyUsuario = new Proxy(usuario, {
  get(objetivo, propiedad) {
    if (propiedad === "nombreCompleto") {
      return `${objetivo.nombre} ${objetivo.apellido}`;
    }
    return objetivo[propiedad];
  }
});

console.log(proxyUsuario.nombreCompleto); // "Juan Pérez"

La API Reflect

Reflect proporciona implementaciones predeterminadas para trampas de Proxy:

// Reflect refleja trampas de Proxy
const obj = { x: 1, y: 2 };

// En lugar de:
obj.x;              // Get
obj.x = 10;         // Set
"x" in obj;         // Has
delete obj.x;       // Delete

// Usa Reflect:
Reflect.get(obj, "x");           // Get
Reflect.set(obj, "x", 10);       // Set
Reflect.has(obj, "x");           // Has
Reflect.deleteProperty(obj, "x"); // Delete

// Útil en manejadores Proxy para llamar comportamiento predeterminado
const proxyLogging = new Proxy(obj, {
  get(objetivo, propiedad, receptor) {
    console.log(`Accediendo a ${propiedad}`);
    return Reflect.get(objetivo, propiedad, receptor); // Comportamiento predeterminado
  },
  
  set(objetivo, propiedad, valor, receptor) {
    console.log(`Estableciendo ${propiedad} = ${valor}`);
    return Reflect.set(objetivo, propiedad, valor, receptor);
  }
});

Proxy Reactivo (Estilo Vue)

function reactivo(obj, alCambiar) {
  return new Proxy(obj, {
    set(objetivo, propiedad, valor, receptor) {
      const valorAntiguo = objetivo[propiedad];
      const resultado = Reflect.set(objetivo, propiedad, valor, receptor);
      
      if (valorAntiguo !== valor) {
        alCambiar(propiedad, valor, valorAntiguo);
      }
      
      return resultado;
    }
  });
}

const estado = reactivo({ cuenta: 0 }, (prop, nuevoVal, viejoVal) => {
  console.log(`${prop} cambió: ${viejoVal} → ${nuevoVal}`);
  // Re-renderizar UI, etc.
});

estado.cuenta = 1; // "cuenta cambió: 0 → 1"
estado.cuenta = 2; // "cuenta cambió: 1 → 2"

Proxy de Función

// Interceptar llamadas a funciones
function crearFuncionConLog(fn) {
  return new Proxy(fn, {
    apply(objetivo, thisArg, args) {
      console.log(`Llamando a ${fn.name} con:`, args);
      const resultado = Reflect.apply(objetivo, thisArg, args);
      console.log(`Resultado:`, resultado);
      return resultado;
    }
  });
}

const sumar = (a, b) => a + b;
const sumarConLog = crearFuncionConLog(sumar);

sumarConLog(2, 3);
// Llamando a sumar con: [2, 3]
// Resultado: 5

// Interceptar llamadas a constructores
class Usuario {
  constructor(nombre) {
    this.nombre = nombre;
  }
}

const UsuarioRastreado = new Proxy(Usuario, {
  construct(objetivo, args) {
    console.log("Creando usuario:", args[0]);
    return Reflect.construct(objetivo, args);
  }
});

new UsuarioRastreado("Alice"); // "Creando usuario: Alice"

Proxies Revocables

// Crear un proxy que puede ser deshabilitado
const { proxy, revoke } = Proxy.revocable(
  { secreto: "password123" },
  {
    get(objetivo, prop) {
      return objetivo[prop];
    }
  }
);

console.log(proxy.secreto); // "password123"

// Revocar acceso
revoke();

console.log(proxy.secreto); // TypeError: No se puede realizar 'get' en un proxy revocado

⚠️ Consideraciones

  • Rendimiento: Los proxies agregan sobrecarga; evítalos en rutas críticas
  • Identidad: proxy !== objetivo, lo cual puede causar problemas
  • Built-ins: Algunos objetos (Map, Set) necesitan manejo especial
  • Invariantes: Las trampas deben respetar las invariantes de JavaScript

💡 Puntos Clave

  • • Proxy intercepta operaciones fundamentales en objetos
  • • Usa trampas (get, set, has, etc.) para personalizar comportamiento
  • • Reflect proporciona implementaciones predeterminadas para operaciones de trampa
  • • Excelente para validación, logging, sistemas reactivos y control de acceso
  • • Los proxies revocables permiten deshabilitar acceso a datos

Práctica y consejos de seguridad

  • • Construye un proxy de logging alrededor de un objeto de servicio; inspecciona cómo receiver afecta propiedades heredadas.
  • • Agrega validación en una trampa set y compara comportamiento con setters de Object.defineProperty.
  • • Crea un proxy revocable para secretos y afirma que el acceso lanza una vez revocado.
  • • Mide código de ruta crítica con y sin proxies para decidir si mantenerlos en producción.