DEV Community

Lucas Pereira de Souza
Lucas Pereira de Souza

Posted on

Proxy e Reflect no JavaScript

logotech

## Desvendando Proxies e Reflect em JavaScript: O Poder de Interceptar e Validar Objetos

No mundo do desenvolvimento backend, especialmente com Node.js, muitas vezes nos deparamos com a necessidade de adicionar comportamentos personalizados a objetos existentes, validar seus acessos ou até mesmo interceptar operações para fins de logging ou segurança. Imagine um cenário onde você quer garantir que um objeto de configuração nunca seja modificado após sua inicialização, ou que um objeto de usuário tenha sempre um formato específico antes de ser salvo no banco de dados. Como podemos alcançar essa flexibilidade e controle sem poluir nosso código com lógica repetitiva?

A resposta reside em duas funcionalidades poderosas do JavaScript moderno: Proxies e o objeto Reflect. Juntos, eles nos permitem criar \"envoltórios\" inteligentes para nossos objetos, interceptando e manipulando operações como leitura, escrita, deleção de propriedades e muito mais.

O Problema: Objetos Rígidos e Validação Manual

Tradicionalmente, para adicionar validação ou comportamentos customizados, recorreríamos a:

  1. Getters e Setters: Úteis para propriedades individuais, mas podem se tornar verbosos e difíceis de gerenciar em objetos complexos com muitas propriedades.
  2. Funções de Validação/Wrapper: Criar funções separadas que validam ou modificam um objeto antes de usá-lo. Isso pode levar à duplicação de código e à dificuldade em garantir que a validação seja sempre aplicada.

Essas abordagens, embora funcionais, podem tornar o código menos legível e mais propenso a erros.

A Solução: Proxies e Reflect

1. Proxies: O Interceptador Inteligente

Um Proxy em JavaScript é um objeto que envolve outro objeto (o \"alvo\") e permite interceptar operações fundamentais (como propriedade lookup, atribuição, enumeração, chamada de função, etc.) aplicadas ao alvo. Ele faz isso através de um \"handler\" — um objeto cujas propriedades são funções (chamadas \"traps\") que definem o comportamento personalizado para operações específicas.

A sintaxe básica é:

const handler = {
  get(target, prop, receiver) {
    // Lógica para interceptar a leitura da propriedade 'prop'
    return Reflect.get(target, prop, receiver); // Operação default
  },
  set(target, prop, value, receiver) {
    // Lógica para interceptar a escrita da propriedade 'prop'
    return Reflect.set(target, prop, value, receiver); // Operação default
  }
  // ... outros traps
};

const target = { name: \"Exemplo\" };
const proxy = new Proxy(target, handler);
Enter fullscreen mode Exit fullscreen mode
  • target: O objeto original que queremos \"envolver\".
  • handler: Um objeto que contém os \"traps\" (funções) para interceptar operações.
  • prop: O nome da propriedade sendo acessada ou modificada.
  • value: O novo valor a ser atribuído (no trap set).
  • receiver: O proxy ou objeto de herança que está sendo usado para a operação.

2. Reflect: A Ponte para Operações Default

O objeto Reflect fornece métodos que correspondem às operações fundamentais que os proxies podem interceptar. Ele é crucial porque, dentro dos traps do handler, precisamos de uma maneira de delegar a operação real para o objeto alvo. Usar Reflect garante que façamos isso de forma consistente e correta, especialmente em cenários de herança.

Por exemplo, em vez de escrever target[prop] = value dentro do trap set, usamos Reflect.set(target, prop, value, receiver). Isso garante que, se target tiver setters personalizados ou se estivermos lidando com protótipos, o comportamento seja o esperado.

Desenvolvimento: Criando um Proxy de Validação

Vamos construir um exemplo prático: um proxy que valida se as propriedades de um objeto User são atribuídas corretamente e impede a modificação de certas propriedades após a criação.

/**
 * Interface para representar um usuário.
 */
interface User {
  id: number;
  name: string;
  email: string;
  readonly isAdmin?: boolean; // Propriedade somente leitura
}

/**
 * Handler para o Proxy que adiciona validação e controle de mutabilidade.
 */
const validationHandler = {
  /**
   * Trap para interceptar a atribuição de propriedades.
   * @param target - O objeto alvo.
   * @param prop - O nome da propriedade a ser definida.
   * @param value - O valor a ser atribuído à propriedade.
   * @param receiver - O objeto proxy ou de herança.
   * @returns true se a atribuição foi bem-sucedida, false caso contrário.
   */
  set(target: User, prop: keyof User, value: any, receiver: any): boolean {
    console.log(`Tentando definir a propriedade \"${String(prop)}\" com o valor:`, value);

    // Validação de email (exemplo simples)
    if (prop === 'email' && typeof value === 'string' && !value.includes('@')) {
      console.error(`Erro: Email inválido \"${value}\". Por favor, forneça um email válido.`);
      return false; // Impede a atribuição
    }

    // Impedir a modificação de propriedades 'readonly'
    if (Object.prototype.hasOwnProperty.call(target, prop) && Object.getOwnPropertyDescriptor(target, prop)?.writable === false) {
       console.error(`Erro: A propriedade \"${String(prop)}\" é somente leitura e não pode ser modificada.`);
       return false; // Impede a atribuição
    }

    // Se todas as validações passarem, use Reflect.set para atribuir o valor ao objeto alvo.
    // O uso de Reflect.set garante que o comportamento padrão (e qualquer lógica customizada
    // no objeto alvo, como setters) seja executado corretamente.
    const success = Reflect.set(target, prop, value, receiver);

    if (success) {
      console.log(`Propriedade \"${String(prop)}\" definida com sucesso para:`, value);
    } else {
      console.error(`Falha ao definir a propriedade \"${String(prop)}\".`);
    }

    return success;
  },

  /**
   * Trap para interceptar a leitura de propriedades.
   * @param target - O objeto alvo.
   * @param prop - O nome da propriedade a ser obtida.
   * @param receiver - O objeto proxy ou de herança.
   * @returns O valor da propriedade.
   */
  get(target: User, prop: keyof User, receiver: any): any {
    console.log(`Acessando a propriedade \"${String(prop)}\"`);
    // Usa Reflect.get para obter o valor da propriedade do objeto alvo.
    // Isso garante o comportamento padrão e respeita getters no objeto alvo.
    return Reflect.get(target, prop, receiver);
  },

  /**
   * Trap para interceptar a deleção de propriedades.
   * Neste exemplo, vamos proibir a deleção de propriedades.
   * @param target - O objeto alvo.
   * @param prop - O nome da propriedade a ser deletada.
   * @returns true se a deleção foi bem-sucedida (ou permitida), false caso contrário.
   */
  deleteProperty(target: User, prop: keyof User): boolean {
    console.error(`Erro: A deleção da propriedade \"${String(prop)}\" não é permitida.`);
    return false; // Impede a deleção
  }
};

// --- Exemplo de Uso ---

// Objeto alvo inicial
const userProfile: User = {
  id: 1,
  name: \"Alice\",
  email: \"alice@example.com\",
  isAdmin: true // Propriedade readonly
};

// Criando o proxy com o handler de validação
const securedUserProfile = new Proxy(userProfile, validationHandler);

// Testando as operações
console.log(\"\n--- Testando o Proxy ---\");

// Leitura de propriedade
console.log(\"Nome do usuário:\", securedUserProfile.name);

// Tentativa de definir uma propriedade válida
securedUserProfile.name = \"Alice Smith\"; // Deve passar

// Tentativa de definir um email inválido
securedUserProfile.email = \"alice-invalid-email\"; // Deve falhar

// Tentativa de definir um email válido
securedUserProfile.email = \"alice.smith@example.com\"; // Deve passar

// Tentativa de modificar uma propriedade 'readonly'
try {
  // @ts-ignore // Ignorando erro de tipagem para demonstração
  securedUserProfile.isAdmin = false; // Deve falhar
} catch (e) {
  console.error(\"Capturado erro ao tentar modificar 'isAdmin':\", e);
}

// Tentativa de deletar uma propriedade
try {
  // @ts-ignore
  delete securedUserProfile.name; // Deve falhar
} catch (e) {
  console.error(\"Capturado erro ao tentar deletar 'name':\", e);
}

console.log(\"\nPerfil final do usuário:\", securedUserProfile);

// Verificando se o objeto original foi modificado (sim, o proxy atua sobre ele)
console.log(\"Perfil original (userProfile):\", userProfile);

Enter fullscreen mode Exit fullscreen mode

Explicação do Código:

  1. Interface User: Define a estrutura esperada para nossos objetos de usuário, incluindo uma propriedade readonly para demonstrar controle de mutabilidade.
  2. validationHandler:
    • set(target, prop, value, receiver): Este é o \"trap\" principal. Ele é acionado toda vez que tentamos atribuir um valor a uma propriedade do proxy.
      • Primeiro, registramos a tentativa.
      • Implementamos uma validação simples para o email. Se o formato for inválido, retornamos false, o que impede a atribuição e sinaliza um erro.
      • Verificamos se a propriedade é readonly usando Object.getOwnPropertyDescriptor. Se for, impedimos a modificação.
      • Se as validações passarem, usamos Reflect.set(target, prop, value, receiver) para realmente definir a propriedade no objeto target. Reflect.set é a forma correta de realizar a operação padrão.
      • Retornamos true ou false para indicar o sucesso ou falha da operação.
    • get(target, prop, receiver): Intercepta a leitura de propriedades. Aqui, simplesmente usamos Reflect.get para obter o valor do target. Poderíamos adicionar logging ou transformar valores aqui.
    • deleteProperty(target, prop): Intercepta a deleção de propriedades. Neste exemplo, proibimos explicitamente a deleção, retornando false.
  3. Exemplo de Uso: Demonstra como criar o userProfile original, envolver ele em um Proxy usando o validationHandler, e então testar as operações de leitura, escrita (válida e inválida) e deleção.

Conclusão

Proxies e Reflect são ferramentas extremamente poderosas para criar código mais robusto, seguro e expressivo em Node.js e no frontend. Eles nos permitem:

  • Validar dados de entrada: Garantir que objetos mantenham um estado consistente.
  • Controlar mutabilidade: Criar objetos \"imutáveis\" ou com partes somente leitura.
  • Adicionar logging e observabilidade: Monitorar acessos e modificações em objetos críticos.
  • Implementar padrões de design: Como \"Lazy Loading\" ou \"Lazy Initialization".
  • Mockar dependências: Em testes unitários, Proxies são essenciais para simular comportamentos de objetos.

Ao dominar essas ferramentas, você eleva sua capacidade de lidar com complexidade e construir aplicações backend mais elegantes e resilientes. Lembre-se de usar Reflect dentro dos traps do seu handler para garantir que as operações padrão sejam executadas corretamente, especialmente em cenários mais complexos.

Top comments (0)