Español

Una guía completa para desarrolladores globales sobre cómo dominar la API Proxy de JavaScript. Aprenda a interceptar y personalizar operaciones de objetos con ejemplos prácticos y consejos de rendimiento.

API Proxy de JavaScript: Una Inmersión Profunda en la Modificación del Comportamiento de Objetos

En el panorama en evolución del JavaScript moderno, los desarrolladores buscan constantemente formas más poderosas y elegantes de administrar e interactuar con los datos. Si bien características como las clases, los módulos y async/await han revolucionado la forma en que escribimos código, existe una poderosa característica de metaprogramación introducida en ECMAScript 2015 (ES6) que a menudo permanece subutilizada: la API Proxy.

La metaprogramación puede sonar intimidante, pero es simplemente el concepto de escribir código que opera sobre otro código. La API Proxy es la herramienta principal de JavaScript para esto, lo que le permite crear un 'proxy' para otro objeto, que puede interceptar y redefinir las operaciones fundamentales de ese objeto. Es como colocar un guardián personalizable frente a un objeto, que le brinda un control completo sobre cómo se accede y se modifica.

Esta guía completa desmitificará la API Proxy. Exploraremos sus conceptos básicos, desglosaremos sus diversas capacidades con ejemplos prácticos y discutiremos casos de uso avanzados y consideraciones de rendimiento. Al final, comprenderá por qué los Proxies son una piedra angular de los frameworks modernos y cómo puede aprovecharlos para escribir código más limpio, más potente y más fácil de mantener.

Comprensión de los Conceptos Básicos: Target, Handler y Traps

La API Proxy se basa en tres componentes fundamentales. Comprender sus roles es la clave para dominar los proxies.

La sintaxis para crear un proxy es sencilla:

const proxy = new Proxy(target, handler);

Veamos un ejemplo muy básico. Crearemos un proxy que simplemente pasa todas las operaciones al objeto target utilizando un handler vacío.


// The original object
const target = {
  message: "Hello, World!"
};

// An empty handler. All operations will be forwarded to the target.
const handler = {};

// The proxy object
const proxy = new Proxy(target, handler);

// Accessing a property on the proxy
console.log(proxy.message); // Output: Hello, World!

// The operation was forwarded to the target
console.log(target.message); // Output: Hello, World!

// Modifying a property through the proxy
proxy.anotherMessage = "Hello, Proxy!";

console.log(proxy.anotherMessage); // Output: Hello, Proxy!
console.log(target.anotherMessage); // Output: Hello, Proxy!

En este ejemplo, el proxy se comporta exactamente como el objeto original. El verdadero poder surge cuando comenzamos a definir traps en el handler.

La Anatomía de un Proxy: Exploración de Traps Comunes

El objeto handler puede contener hasta 13 traps diferentes, cada uno correspondiente a un método interno fundamental de los objetos JavaScript. Exploremos los más comunes y útiles.

Traps de Acceso a Propiedades

1. `get(target, property, receiver)`

Este es posiblemente el trap más utilizado. Se activa cuando se lee una propiedad del proxy.

Ejemplo: Valores predeterminados para propiedades inexistentes.


const user = {
  firstName: 'John',
  lastName: 'Doe',
  age: 30
};

const userHandler = {
  get(target, property) {
    // If the property exists on the target, return it.
    // Otherwise, return a default message.
    return property in target ? target[property] : `Property '${property}' does not exist.`;
  }
};

const userProxy = new Proxy(user, userHandler);

console.log(userProxy.firstName); // Output: John
console.log(userProxy.age);       // Output: 30
console.log(userProxy.country);   // Output: Property 'country' does not exist.

2. `set(target, property, value, receiver)`

El trap set se llama cuando a una propiedad del proxy se le asigna un valor. Es perfecto para la validación, el registro o la creación de objetos de solo lectura.

Ejemplo: Validación de datos.


const person = {
  name: 'Jane Doe',
  age: 25
};

const validationHandler = {
  set(target, property, value) {
    if (property === 'age') {
      if (typeof value !== 'number' || !Number.isInteger(value)) {
        throw new TypeError('Age must be an integer.');
      }
      if (value <= 0) {
        throw new RangeError('Age must be a positive number.');
      }
    }

    // If validation passes, set the value on the target object.
    target[property] = value;

    // Indicate success.
    return true;
  }
};

const personProxy = new Proxy(person, validationHandler);

personProxy.age = 30; // This is valid
console.log(personProxy.age); // Output: 30

try {
  personProxy.age = 'thirty'; // Throws TypeError
} catch (e) {
  console.error(e.message); // Output: Age must be an integer.
}

try {
  personProxy.age = -5; // Throws RangeError
} catch (e) {
  console.error(e.message); // Output: Age must be a positive number.
}

3. `has(target, property)`

Este trap intercepta el operador in. Le permite controlar qué propiedades parecen existir en un objeto.

Ejemplo: Ocultar propiedades 'privadas'.

En JavaScript, una convención común es anteponer un guión bajo (_) a las propiedades privadas. Podemos usar el trap has para ocultarlas del operador in.


const secretData = {
  _apiKey: 'xyz123abc',
  publicKey: 'pub456def',
  id: 1
};

const hidingHandler = {
  has(target, property) {
    if (property.startsWith('_')) {
      return false; // Pretend it doesn't exist
    }
    return property in target;
  }
};

const dataProxy = new Proxy(secretData, hidingHandler);

console.log('publicKey' in dataProxy); // Output: true
console.log('_apiKey' in dataProxy);   // Output: false (even though it's on the target)
console.log('id' in dataProxy);        // Output: true

Nota: Esto solo afecta al operador in. El acceso directo como dataProxy._apiKey aún funcionaría a menos que también implemente un trap get correspondiente.

4. `deleteProperty(target, property)`

Este trap se ejecuta cuando se elimina una propiedad utilizando el operador delete. Es útil para evitar la eliminación de propiedades importantes.

El trap debe devolver true para una eliminación exitosa o false para una fallida.

Ejemplo: Evitar la eliminación de propiedades.


const immutableConfig = {
  databaseUrl: 'prod.db.server',
  port: 8080
};

const deletionGuardHandler = {
  deleteProperty(target, property) {
    if (property in target) {
      console.warn(`Attempted to delete protected property: '${property}'. Operation denied.`);
      return false;
    }
    return true; // Property didn't exist anyway
  }
};

const configProxy = new Proxy(immutableConfig, deletionGuardHandler);

delete configProxy.port;
// Console output: Attempted to delete protected property: 'port'. Operation denied.

console.log(configProxy.port); // Output: 8080 (It wasn't deleted)

Traps de Enumeración y Descripción de Objetos

5. `ownKeys(target)`

Este trap se activa mediante operaciones que obtienen la lista de las propias propiedades de un objeto, como Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() y Reflect.ownKeys().

Ejemplo: Filtrado de claves.

Combinemos esto con nuestro ejemplo anterior de propiedad 'privada' para ocultarlas por completo.


const secretData = {
  _apiKey: 'xyz123abc',
  publicKey: 'pub456def',
  id: 1
};

const keyHidingHandler = {
  has(target, property) {
    return !property.startsWith('_') && property in target;
  },
  ownKeys(target) {
    return Reflect.ownKeys(target).filter(key => !key.startsWith('_'));
  },
  get(target, property, receiver) {
    // Also prevent direct access
    if (property.startsWith('_')) {
      return undefined;
    }
    return Reflect.get(target, property, receiver);
  }
};

const fullProxy = new Proxy(secretData, keyHidingHandler);

console.log(Object.keys(fullProxy)); // Output: ['publicKey', 'id']
console.log('publicKey' in fullProxy); // Output: true
console.log('_apiKey' in fullProxy);   // Output: false
console.log(fullProxy._apiKey);      // Output: undefined

Observe que estamos usando Reflect aquí. El objeto Reflect proporciona métodos para operaciones JavaScript interceptables, y sus métodos tienen los mismos nombres y firmas que los traps de proxy. Es una práctica recomendada utilizar Reflect para reenviar la operación original al target, asegurando que el comportamiento predeterminado se mantenga correctamente.

Traps de Función y Constructor

Los proxies no se limitan a objetos simples. Cuando el target es una función, puede interceptar llamadas y construcciones.

6. `apply(target, thisArg, argumentsList)`

Este trap se llama cuando se ejecuta un proxy de una función. Intercepta la llamada a la función.

Ejemplo: Registro de llamadas de función y sus argumentos.


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

const loggingHandler = {
  apply(target, thisArg, argumentsList) {
    console.log(`Calling function '${target.name}' with arguments: ${argumentsList}`);
    // Execute the original function with the correct context and arguments
    const result = Reflect.apply(target, thisArg, argumentsList);
    console.log(`Function '${target.name}' returned: ${result}`);
    return result;
  }
};

const proxiedSum = new Proxy(sum, loggingHandler);

proxiedSum(5, 10);
// Console output:
// Calling function 'sum' with arguments: 5,10
// Function 'sum' returned: 15

7. `construct(target, argumentsList, newTarget)`

Este trap intercepta el uso del operador new en un proxy de una clase o función.

Ejemplo: Implementación del patrón Singleton.


class MyDatabaseConnection {
  constructor(url) {
    this.url = url;
    console.log(`Connecting to ${this.url}...`);
  }
}

let instance;

const singletonHandler = {
  construct(target, argumentsList) {
    if (!instance) {
      console.log('Creating new instance.');
      instance = Reflect.construct(target, argumentsList);
    }
    console.log('Returning existing instance.');
    return instance;
  }
};

const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);

const conn1 = new ProxiedConnection('db://primary');
// Console output:
// Creating new instance.
// Connecting to db://primary...
// Returning existing instance.

const conn2 = new ProxiedConnection('db://secondary'); // URL will be ignored
// Console output:
// Returning existing instance.

console.log(conn1 === conn2); // Output: true
console.log(conn1.url); // Output: db://primary
console.log(conn2.url); // Output: db://primary

Casos de Uso Prácticos y Patrones Avanzados

Ahora que hemos cubierto los traps individuales, veamos cómo se pueden combinar para resolver problemas del mundo real.

1. Abstracción de API y Transformación de Datos

Las API a menudo devuelven datos en un formato que no coincide con las convenciones de su aplicación (por ejemplo, snake_case vs. camelCase). Un proxy puede manejar esta conversión de forma transparente.


function snakeToCamel(s) {
  return s.replace(/(_\w)/g, (m) => m[1].toUpperCase());
}

// Imagine this is our raw data from an API
const apiResponse = {
  user_id: 123,
  first_name: 'Alice',
  last_name: 'Wonderland',
  account_status: 'active'
};

const camelCaseHandler = {
  get(target, property) {
    const camelCaseProperty = snakeToCamel(property);
    // Check if the camelCase version exists directly
    if (camelCaseProperty in target) {
      return target[camelCaseProperty];
    }
    // Fallback to original property name
    if (property in target) {
      return target[property];
    }
    return undefined;
  }
};

const userModel = new Proxy(apiResponse, camelCaseHandler);

// We can now access properties using camelCase, even though they are stored as snake_case
console.log(userModel.userId);        // Output: 123
console.log(userModel.firstName);     // Output: Alice
console.log(userModel.accountStatus); // Output: active

2. Observables y Enlace de Datos (El Núcleo de los Frameworks Modernos)

Los proxies son el motor detrás de los sistemas de reactividad en frameworks modernos como Vue 3. Cuando cambia una propiedad en un objeto de estado proxy, el trap set se puede usar para activar actualizaciones en la interfaz de usuario u otras partes de la aplicación.

Aquí hay un ejemplo muy simplificado:


function createObservable(target, callback) {
  const handler = {
    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      callback(prop, value); // Trigger the callback on change
      return result;
    }
  };
  return new Proxy(target, handler);
}

const state = {
  count: 0,
  message: 'Hello'
};

function render(prop, value) {
  console.log(`CHANGE DETECTED: The property '${prop}' was set to '${value}'. Re-rendering UI...`);
}

const observableState = createObservable(state, render);

observableState.count = 1;
// Console output: CHANGE DETECTED: The property 'count' was set to '1'. Re-rendering UI...

observableState.message = 'Goodbye';
// Console output: CHANGE DETECTED: The property 'message' was set to 'Goodbye'. Re-rendering UI...

3. Índices de Array Negativos

Un ejemplo clásico y divertido es extender el comportamiento nativo de los arrays para admitir índices negativos, donde -1 se refiere al último elemento, similar a lenguajes como Python.


function createNegativeArrayProxy(arr) {
  const handler = {
    get(target, property) {
      const index = Number(property);
      if (!Number.isNaN(index) && index < 0) {
        // Convert negative index to a positive one from the end
        property = String(target.length + index);
      }
      return Reflect.get(target, property);
    }
  };
  return new Proxy(arr, handler);
}

const originalArray = ['a', 'b', 'c', 'd', 'e'];
const proxiedArray = createNegativeArrayProxy(originalArray);

console.log(proxiedArray[0]);  // Output: a
console.log(proxiedArray[-1]); // Output: e
console.log(proxiedArray[-2]); // Output: d
console.log(proxiedArray.length); // Output: 5

Consideraciones de Rendimiento y Mejores Prácticas

Si bien los proxies son increíblemente poderosos, no son una panacea. Es fundamental comprender sus implicaciones.

La Sobrecarga de Rendimiento

Un proxy introduce una capa de indirección. Cada operación en un objeto proxy debe pasar por el handler, lo que agrega una pequeña cantidad de sobrecarga en comparación con una operación directa en un objeto simple. Para la mayoría de las aplicaciones (como la validación de datos o la reactividad a nivel de framework), esta sobrecarga es insignificante. Sin embargo, en código crítico para el rendimiento, como un bucle ajustado que procesa millones de elementos, esto puede convertirse en un cuello de botella. Siempre realice pruebas de referencia si el rendimiento es una preocupación principal.

Invariantes de Proxy

Un trap no puede mentir por completo sobre la naturaleza del objeto target. JavaScript aplica un conjunto de reglas llamadas 'invariantes' que los traps de proxy deben obedecer. La violación de un invariante dará como resultado un TypeError.

Por ejemplo, un invariante para el trap deleteProperty es que no puede devolver true (lo que indica éxito) si la propiedad correspondiente en el objeto target no es configurable. Esto evita que el proxy afirme que eliminó una propiedad que no se puede eliminar.


const target = {};
Object.defineProperty(target, 'unbreakable', { value: 10, configurable: false });

const handler = {
  deleteProperty(target, prop) {
    // This will violate the invariant
    return true;
  }
};

const proxy = new Proxy(target, handler);

try {
  delete proxy.unbreakable; // This will throw an error
} catch (e) {
  console.error(e.message);
  // Output: 'deleteProperty' on proxy: returned true for non-configurable property 'unbreakable'
}

Cuándo Usar Proxies (y Cuándo No)

Proxies Revocables

Para escenarios en los que es posible que deba 'desactivar' un proxy (por ejemplo, por razones de seguridad o administración de memoria), JavaScript proporciona Proxy.revocable(). Devuelve un objeto que contiene tanto el proxy como una función revoke.


const target = { data: 'sensitive' };
const handler = {};

const { proxy, revoke } = Proxy.revocable(target, handler);

console.log(proxy.data); // Output: sensitive

// Now, we revoke the proxy's access
revoke();

try {
  console.log(proxy.data); // This will throw an error
} catch (e) {
  console.error(e.message);
  // Output: Cannot perform 'get' on a proxy that has been revoked
}

Proxies vs. Otras Técnicas de Metaprogramación

Antes de Proxies, los desarrolladores usaban otros métodos para lograr objetivos similares. Es útil comprender cómo se comparan los Proxies.

`Object.defineProperty()`

Object.defineProperty() modifica un objeto directamente definiendo getters y setters para propiedades específicas. Los proxies, por otro lado, no modifican el objeto original en absoluto; lo envuelven.

Conclusión: El Poder de la Virtualización

La API Proxy de JavaScript es más que una característica inteligente; es un cambio fundamental en la forma en que podemos diseñar e interactuar con los objetos. Al permitirnos interceptar y personalizar las operaciones fundamentales, los Proxies abren la puerta a un mundo de patrones poderosos: desde la validación y transformación de datos sin problemas hasta los sistemas reactivos que impulsan las interfaces de usuario modernas.

Si bien tienen un pequeño costo de rendimiento y un conjunto de reglas a seguir, su capacidad para crear abstracciones limpias, desacopladas y poderosas es inigualable. Al virtualizar objetos, puede crear sistemas que sean más robustos, mantenibles y expresivos. La próxima vez que se enfrente a un desafío complejo que involucre la administración, validación u observabilidad de datos, considere si un Proxy es la herramienta adecuada para el trabajo. Podría ser la solución más elegante de su conjunto de herramientas.