Português

Um guia abrangente para desenvolvedores globais sobre como dominar a API Proxy do JavaScript. Aprenda a interceptar e personalizar operações de objetos com exemplos práticos.

API Proxy do JavaScript: Um Mergulho Profundo na Modificação do Comportamento de Objetos

Na paisagem em evolução do JavaScript moderno, os desenvolvedores estão constantemente buscando maneiras mais poderosas e elegantes de gerenciar e interagir com dados. Embora recursos como classes, módulos e async/await tenham revolucionado a forma como escrevemos código, existe um poderoso recurso de metaprogramação introduzido no ECMAScript 2015 (ES6) que muitas vezes permanece subutilizado: a API Proxy.

Metaprogramação pode parecer intimidante, mas é simplesmente o conceito de escrever código que opera em outro código. A API Proxy é a principal ferramenta do JavaScript para isso, permitindo que você crie um 'proxy' para outro objeto, que pode interceptar e redefinir operações fundamentais para esse objeto. É como colocar um porteiro personalizável na frente de um objeto, dando a você controle completo sobre como ele é acessado e modificado.

Este guia abrangente irá desmistificar a API Proxy. Exploraremos seus principais conceitos, detalharemos suas várias capacidades com exemplos práticos e discutiremos casos de uso avançados e considerações de desempenho. Ao final, você entenderá por que os Proxies são uma pedra angular das estruturas modernas e como você pode aproveitá-los para escrever código mais limpo, poderoso e fácil de manter.

Entendendo os Conceitos Centrais: Target, Handler e Traps

A API Proxy é construída sobre três componentes fundamentais. Entender seus papéis é a chave para dominar os proxies.

A sintaxe para criar um proxy é direta:

const proxy = new Proxy(target, handler);

Vamos dar uma olhada em um exemplo muito básico. Criaremos um proxy que simplesmente passa todas as operações para o objeto target usando um handler vazio.


// O objeto original
const target = {
  message: "Olá, Mundo!"
};

// Um handler vazio. Todas as operações serão encaminhadas para o target.
const handler = {};

// O objeto proxy
const proxy = new Proxy(target, handler);

// Acessando uma propriedade no proxy
console.log(proxy.message); // Output: Olá, Mundo!

// A operação foi encaminhada para o target
console.log(target.message); // Output: Olá, Mundo!

// Modificando uma propriedade através do proxy
proxy.anotherMessage = "Olá, Proxy!";

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

Neste exemplo, o proxy se comporta exatamente como o objeto original. O verdadeiro poder vem quando começamos a definir traps no handler.

A Anatomia de um Proxy: Explorando Traps Comuns

O objeto handler pode conter até 13 traps diferentes, cada uma correspondendo a um método interno fundamental dos objetos JavaScript. Vamos explorar as mais comuns e úteis.

Traps de Acesso a Propriedades

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

Esta é, sem dúvida, a trap mais usada. É acionada quando uma propriedade do proxy é lida.

Exemplo: Valores padrão para propriedades inexistentes.


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

const userHandler = {
  get(target, property) {
    // Se a propriedade existir no target, retorne-a.
    // Caso contrário, retorne uma mensagem padrão.
    return property in target ? target[property] : `A propriedade '${property}' não existe.`;
  }
};

const userProxy = new Proxy(user, userHandler);

console.log(userProxy.firstName); // Output: John
console.log(userProxy.age);       // Output: 30
console.log(userProxy.country);   // Output: A propriedade 'country' não existe.

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

A trap set é chamada quando uma propriedade do proxy recebe um valor. É perfeito para validação, registro ou criação de objetos somente leitura.

Exemplo: Validação de dados.


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('A idade deve ser um inteiro.');
      }
      if (value <= 0) {
        throw new RangeError('A idade deve ser um número positivo.');
      }
    }

    // Se a validação for aprovada, defina o valor no objeto target.
    target[property] = value;

    // Indique o sucesso.
    return true;
  }
};

const personProxy = new Proxy(person, validationHandler);

personProxy.age = 30; // Isto é válido
console.log(personProxy.age); // Output: 30

try {
  personProxy.age = 'trinta'; // Lança TypeError
} catch (e) {
  console.error(e.message); // Output: A idade deve ser um inteiro.
}

try {
  personProxy.age = -5; // Lança RangeError
} catch (e) {
  console.error(e.message); // Output: A idade deve ser um número positivo.
}

3. `has(target, property)`

Esta trap intercepta o operador in. Ele permite que você controle quais propriedades parecem existir em um objeto.

Exemplo: Ocultando propriedades 'privadas'.

Em JavaScript, uma convenção comum é prefixar propriedades privadas com um sublinhado (_). Podemos usar a trap has para ocultá-las do operador in.


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

const hidingHandler = {
  has(target, property) {
    if (property.startsWith('_')) {
      return false; // Finja que não existe
    }
    return property in target;
  }
};

const dataProxy = new Proxy(secretData, hidingHandler);

console.log('publicKey' in dataProxy); // Output: true
console.log('_apiKey' in dataProxy);   // Output: false (mesmo que esteja no target)
console.log('id' in dataProxy);        // Output: true

Nota: Isso afeta apenas o operador in. O acesso direto como dataProxy._apiKey ainda funcionaria, a menos que você também implemente uma trap get correspondente.

4. `deleteProperty(target, property)`

Esta trap é executada quando uma propriedade é excluída usando o operador delete. É útil para impedir a exclusão de propriedades importantes.

A trap deve retornar true para uma exclusão bem-sucedida ou false para uma falha.

Exemplo: Impedindo a exclusão de propriedades.


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

const deletionGuardHandler = {
  deleteProperty(target, property) {
    if (property in target) {
      console.warn(`Tentativa de excluir a propriedade protegida: '${property}'. Operação negada.`);
      return false;
    }
    return true; // A propriedade não existia de qualquer maneira
  }
};

const configProxy = new Proxy(immutableConfig, deletionGuardHandler);

delete configProxy.port;
// Saída do console: Tentativa de excluir a propriedade protegida: 'port'. Operação negada.

console.log(configProxy.port); // Output: 8080 (Não foi excluído)

Traps de Enumeração e Descrição de Objetos

5. `ownKeys(target)`

Esta trap é acionada por operações que obtêm a lista das próprias propriedades de um objeto, como Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() e Reflect.ownKeys().

Exemplo: Filtrando chaves.

Vamos combinar isso com nosso exemplo anterior de propriedade 'privada' para ocultá-las totalmente.


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) {
    // Também impede o acesso direto
    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 aqui. O objeto Reflect fornece métodos para operações JavaScript interceptáveis, e seus métodos têm os mesmos nomes e assinaturas que as traps de proxy. É uma prática recomendada usar Reflect para encaminhar a operação original para o target, garantindo que o comportamento padrão seja mantido corretamente.

Traps de Função e Construtor

Os proxies não se limitam a objetos simples. Quando o target é uma função, você pode interceptar chamadas e construções.

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

Esta trap é chamada quando um proxy de uma função é executado. Ele intercepta a chamada de função.

Exemplo: Registrando chamadas de função e seus argumentos.


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

const loggingHandler = {
  apply(target, thisArg, argumentsList) {
    console.log(`Chamando a função '${target.name}' com argumentos: ${argumentsList}`);
    // Execute a função original com o contexto e argumentos corretos
    const result = Reflect.apply(target, thisArg, argumentsList);
    console.log(`A função '${target.name}' retornou: ${result}`);
    return result;
  }
};

const proxiedSum = new Proxy(sum, loggingHandler);

proxiedSum(5, 10);
// Saída do console:
// Chamando a função 'sum' com argumentos: 5,10
// A função 'sum' retornou: 15

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

Esta trap intercepta o uso do operador new em um proxy de uma classe ou função.

Exemplo: Implementação do padrão Singleton.


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

let instance;

const singletonHandler = {
  construct(target, argumentsList) {
    if (!instance) {
      console.log('Criando nova instância.');
      instance = Reflect.construct(target, argumentsList);
    }
    console.log('Retornando instância existente.');
    return instance;
  }
};

const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);

const conn1 = new ProxiedConnection('db://primary');
// Saída do console:
// Criando nova instância.
// Conectando a db://primary...
// Retornando instância existente.

const conn2 = new ProxiedConnection('db://secondary'); // O URL será ignorado
// Saída do console:
// Retornando instância existente.

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

Casos de Uso Práticos e Padrões Avançados

Agora que cobrimos as traps individuais, vamos ver como elas podem ser combinadas para resolver problemas do mundo real.

1. Abstração de API e Transformação de Dados

As APIs geralmente retornam dados em um formato que não corresponde às convenções do seu aplicativo (por exemplo, snake_case vs. camelCase). Um proxy pode lidar com essa conversão de forma transparente.


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

// Imagine que estes são nossos dados brutos de uma API
const apiResponse = {
  user_id: 123,
  first_name: 'Alice',
  last_name: 'Wonderland',
  account_status: 'active'
};

const camelCaseHandler = {
  get(target, property) {
    const camelCaseProperty = snakeToCamel(property);
    // Verifique se a versão camelCase existe diretamente
    if (camelCaseProperty in target) {
      return target[camelCaseProperty];
    }
    // Volte para o nome da propriedade original
    if (property in target) {
      return target[property];
    }
    return undefined;
  }
};

const userModel = new Proxy(apiResponse, camelCaseHandler);

// Agora podemos acessar as propriedades usando camelCase, mesmo que estejam armazenadas como snake_case
console.log(userModel.userId);        // Output: 123
console.log(userModel.firstName);     // Output: Alice
console.log(userModel.accountStatus); // Output: active

2. Observables e Data Binding (O Núcleo das Estruturas Modernas)

Os proxies são o motor por trás dos sistemas de reatividade em estruturas modernas como o Vue 3. Quando você altera uma propriedade em um objeto de estado proxied, a trap set pode ser usada para acionar atualizações na UI ou em outras partes do aplicativo.

Aqui está um exemplo altamente simplificado:


function createObservable(target, callback) {
  const handler = {
    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      callback(prop, value); // Acione o callback na alteração
      return result;
    }
  };
  return new Proxy(target, handler);
}

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

function render(prop, value) {
  console.log(`MUDANÇA DETECTADA: A propriedade '${prop}' foi definida como '${value}'. Re-renderizando a UI...`);
}

const observableState = createObservable(state, render);

observableState.count = 1;
// Saída do console: MUDANÇA DETECTADA: A propriedade 'count' foi definida como '1'. Re-renderizando a UI...

observableState.message = 'Goodbye';
// Saída do console: MUDANÇA DETECTADA: A propriedade 'message' foi definida como 'Goodbye'. Re-renderizando a UI...

3. Índices de Array Negativos

Um exemplo clássico e divertido é estender o comportamento nativo do array para suportar índices negativos, onde -1 se refere ao último elemento, semelhante a linguagens como Python.


function createNegativeArrayProxy(arr) {
  const handler = {
    get(target, property) {
      const index = Number(property);
      if (!Number.isNaN(index) && index < 0) {
        // Converter o índice negativo para um positivo a partir do final
        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

Considerações de Desempenho e Melhores Práticas

Embora os proxies sejam incrivelmente poderosos, eles não são uma bala mágica. É crucial entender suas implicações.

A Sobrecarga de Desempenho

Um proxy introduz uma camada de indireção. Cada operação em um objeto proxied deve passar pelo handler, o que adiciona uma pequena quantidade de sobrecarga em comparação com uma operação direta em um objeto simples. Para a maioria dos aplicativos (como validação de dados ou reatividade no nível do framework), essa sobrecarga é insignificante. No entanto, em código crítico para o desempenho, como um loop apertado processando milhões de itens, isso pode se tornar um gargalo. Sempre faça benchmark se o desempenho for uma preocupação primária.

Invariantes de Proxy

Uma trap não pode mentir completamente sobre a natureza do objeto target. O JavaScript impõe um conjunto de regras chamadas 'invariantes' que as traps de proxy devem obedecer. Violar uma invariante resultará em um TypeError.

Por exemplo, uma invariante para a trap deleteProperty é que ela não pode retornar true (indicando sucesso) se a propriedade correspondente no objeto target não for configurável. Isso impede que o proxy afirme que excluiu uma propriedade que não pode ser excluída.


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

const handler = {
  deleteProperty(target, prop) {
    // Isso violará a invariante
    return true;
  }
};

const proxy = new Proxy(target, handler);

try {
  delete proxy.unbreakable; // Isso lançará um erro
} catch (e) {
  console.error(e.message);
  // Output: 'deleteProperty' on proxy: returned true for non-configurable property 'unbreakable'
}

Quando Usar Proxies (e Quando Não)

Proxies Revogáveis

Para cenários onde você pode precisar 'desligar' um proxy (por exemplo, por motivos de segurança ou gerenciamento de memória), o JavaScript fornece Proxy.revocable(). Ele retorna um objeto contendo tanto o proxy quanto uma função revoke.


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

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

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

// Agora, revogamos o acesso do proxy
revoke();

try {
  console.log(proxy.data); // Isso lançará um erro
} catch (e) {
  console.error(e.message);
  // Output: Cannot perform 'get' on a proxy that has been revoked
}

Proxies vs. Outras Técnicas de Metaprogramação

Antes dos Proxies, os desenvolvedores usavam outros métodos para atingir objetivos semelhantes. É útil entender como os Proxies se comparam.

`Object.defineProperty()`

Object.defineProperty() modifica um objeto diretamente, definindo getters e setters para propriedades específicas. Os proxies, por outro lado, não modificam o objeto original; eles o envolvem.

Conclusão: O Poder da Virtualização

A API Proxy do JavaScript é mais do que apenas um recurso inteligente; é uma mudança fundamental em como podemos projetar e interagir com objetos. Ao nos permitir interceptar e personalizar operações fundamentais, os Proxies abrem a porta para um mundo de padrões poderosos: desde validação e transformação de dados contínuas até os sistemas reativos que impulsionam as interfaces de usuário modernas.

Embora eles venham com um pequeno custo de desempenho e um conjunto de regras a seguir, sua capacidade de criar abstrações limpas, desacopladas e poderosas é incomparável. Ao virtualizar objetos, você pode construir sistemas mais robustos, fáceis de manter e expressivos. Da próxima vez que você enfrentar um desafio complexo envolvendo gerenciamento de dados, validação ou observabilidade, considere se um Proxy é a ferramenta certa para o trabalho. Pode ser a solução mais elegante em seu kit de ferramentas.

API Proxy do JavaScript: Um Mergulho Profundo na Modificação do Comportamento de Objetos | MLOG