Metaprogramação: Proxy e Reflect
LidoNesta aula, exploramos a metaprogramação em JavaScript com foco nos objetos Proxy e Reflect. Aprendemos a interceptar operações fundamentais de objetos, aplicamos isso em casos práticos como validação e logging, e discutimos cuidados importantes, incluindo questões de desempenho e compatibilidade.
Bem-vindo à aula sobre metaprogramação em JavaScript! Nesta aula, vamos mergulhar em dois recursos poderosos: Proxy e Reflect. Esses recursos permitem que você intercepte e personalize operações fundamentais em objetos, como leitura de propriedades, atribuição, invocação de funções e muito mais. Isso abre um leque de possibilidades para criar código mais flexível, reativo e até mesmo para implementar padrões avançados de design.
Metaprogramação é a capacidade de um programa de se modificar ou inspecionar a si mesmo. Em JavaScript, os objetos Proxy e Reflect são ferramentas essenciais para essa prática, pois permitem que você defina comportamentos personalizados para operações internas da linguagem. Vamos entender como eles funcionam e como aplicá-los de forma eficaz e segura.
Interceptando operações
Um Proxy é um objeto que envolve outro objeto (o alvo) e intercepta operações realizadas sobre ele, permitindo que você defina comportamentos personalizados. As interceptações são definidas por meio de funções chamadas traps, que são métodos de um objeto manipulador (handler). Cada trap corresponde a uma operação interna específica do JavaScript.
O objeto Reflect fornece métodos estáticos que espelham as operações internas interceptáveis. Usar Reflect dentro dos traps é uma boa prática, pois garante que o comportamento padrão seja mantido quando você não deseja modificar completamente a operação. Por exemplo, o trap get é chamado quando uma propriedade é lida. Podemos interceptar essa leitura e retornar um valor diferente.
// Exemplo básico de Proxy interceptando leitura de propriedade
const alvo = { mensagem: "Olá" };
const manipulador = {
get(alvo, propriedade, receptor) {
if (propriedade === "mensagem") {
return "Interceptado! " + alvo[propriedade];
}
// Usando Reflect para manter o comportamento padrão
return Reflect.get(alvo, propriedade, receptor);
}
};
const proxy = new Proxy(alvo, manipulador);
console.log(proxy.mensagem); // "Interceptado! Olá"
console.log(proxy.outra); // undefined (comportamento padrão)
No exemplo acima, interceptamos a leitura da propriedade mensagem e modificamos o retorno. Para as demais propriedades, delegamos ao Reflect.get, que preserva o comportamento original. Isso é crucial para não quebrar a semântica esperada.
Além de get, existem muitos outros traps, como set (atribuição), has (operador in), deleteProperty, apply (chamada de função), construct (com new), entre outros. Vamos ver um exemplo com set para validar valores antes de atribuir:
// Validação de atribuição com Proxy
const usuario = { nome: "", idade: 0 };
const validador = {
set(alvo, propriedade, valor, receptor) {
if (propriedade === "idade" && (typeof valor !== "number" || valor < 0)) {
throw new TypeError("Idade deve ser um número positivo");
}
return Reflect.set(alvo, propriedade, valor, receptor);
}
};
const proxyUsuario = new Proxy(usuario, validador);
proxyUsuario.nome = "Ana";
proxyUsuario.idade = 25; // OK
// proxyUsuario.idade = -5; // Lançaria TypeError
Esse padrão é muito útil para garantir integridade de dados em aplicações.
Casos de uso
Os Proxies são extremamente versáteis e podem ser usados em muitos cenários práticos. Alguns dos usos mais comuns incluem:
- Validação e sanitização de dados: como vimos, interceptar
setpara garantir que os valores atribuídos atendam a critérios. - Logging e observabilidade: registrar todas as operações realizadas em um objeto, útil para depuração e monitoramento.
- Cache e memoização: interceptar chamadas de função e armazenar resultados para evitar recálculos.
- Controle de acesso: restringir acesso a certas propriedades ou métodos baseado em regras de negócio.
- Implementação de APIs reativas: frameworks como Vue.js usam Proxies para detectar mudanças em objetos e atualizar a interface.
Vamos explorar um exemplo de logging: podemos criar um Proxy que registra todas as leituras e escritas de propriedade.
// Logging de operações com Proxy
const objeto = { a: 1, b: 2 };
const logHandler = {
get(alvo, propriedade, receptor) {
console.log(`Leitura da propriedade "${propriedade}"`);
return Reflect.get(alvo, propriedade, receptor);
},
set(alvo, propriedade, valor, receptor) {
console.log(`Escrita da propriedade "${propriedade}" = ${valor}`);
return Reflect.set(alvo, propriedade, valor, receptor);
}
};
const proxyLog = new Proxy(objeto, logHandler);
let x = proxyLog.a; // Log: Leitura da propriedade "a"
proxyLog.b = 10; // Log: Escrita da propriedade "b" = 10
Outro exemplo interessante é a criação de objetos com propriedades privadas simuladas. Embora JavaScript não tenha suporte nativo a propriedades privadas (antes do #), podemos usar Proxy para ocultar propriedades que começam com _:
// Simulando propriedades privadas com Proxy
const obj = { _segredo: "valor secreto", publico: "visível" };
const privadoHandler = {
get(alvo, propriedade, receptor) {
if (propriedade.startsWith("_")) {
throw new Error("Acesso negado a propriedade privada");
}
return Reflect.get(alvo, propriedade, receptor);
},
set(alvo, propriedade, valor, receptor) {
if (propriedade.startsWith("_")) {
throw new Error("Acesso negado a propriedade privada");
}
return Reflect.set(alvo, propriedade, valor, receptor);
}
};
const proxyPrivado = new Proxy(obj, privadoHandler);
console.log(proxyPrivado.publico); // "visível"
// console.log(proxyPrivado._segredo); // Lança erro
Cuidados
Apesar do poder dos Proxies, é preciso ter cautela ao usá-los. Aqui estão alguns cuidados importantes:
- Desempenho: Proxies adicionam uma camada de sobrecarga a cada operação interceptada. Em código crítico de desempenho, o uso excessivo pode impactar negativamente a velocidade. Use apenas onde necessário.
- Compatibilidade: Embora
ProxyeReflectsejam suportados em todos os navegadores modernos, ambientes mais antigos (como IE11) não os suportam. Verifique o suporte se você precisa de compatibilidade ampla. - Invariantes: Os Proxies têm invariantes que não podem ser violados. Por exemplo, se a propriedade alvo for não configurável e não gravável, o trap
setnão pode alterar seu valor. Tentar violar essas regras lança umTypeError. É importante entender essas restrições para evitar erros. - Igualdade e identidade: Um Proxy é um objeto diferente do alvo. Isso pode causar problemas se você espera que
proxy === alvo. Use o Proxy de forma consistente em todo o código. - Escopo de uso: Evite usar Proxies em excesso em código que será mantido por muitas pessoas, pois pode tornar o comportamento do código menos previsível. Documente bem o uso.
Vejamos um exemplo de invariante: se uma propriedade é não configurável e não gravável, o trap set não pode alterá-la.
// Invariante: propriedade não configurável e não gravável
const alvo = {};
Object.defineProperty(alvo, "fixa", {
value: 42,
writable: false,
configurable: false
});
const handler = {
set(alvo, prop, valor) {
// Tenta alterar a propriedade "fixa"
return Reflect.set(alvo, prop, valor);
}
};
const proxy = new Proxy(alvo, handler);
proxy.fixa = 100; // Em modo estrito, lança TypeError; em modo não estrito, falha silenciosamente
console.log(proxy.fixa); // 42 (inalterado)
Além disso, lembre-se de que Reflect também pode ser usado sem Proxy para operações de objeto mais limpas, como Reflect.get e Reflect.set, que retornam true/false em vez de lançar exceções.
Boas práticas e observações finais
Para encerrar, aqui vão algumas boas práticas ao trabalhar com metaprogramação:
- Sempre que possível, use
Reflectdentro dos traps para garantir o comportamento padrão e evitar efeitos colaterais inesperados. - Documente claramente o propósito de cada Proxy, pois o código pode se tornar difícil de entender.
- Considere o impacto no desempenho e meça se necessário.
- Teste exaustivamente os invariantes e casos extremos.
- Use Proxies para criar APIs elegantes, mas não exagere: se um padrão simples de função resolver, prefira-o.
Com essas ferramentas, você pode levar seu JavaScript a um novo nível, criando código mais expressivo e robusto. Continue praticando e explorando os limites da metaprogramação!
Referências
- MDN: Proxy
- MDN: Reflect
- MDN: Trap get
- MDN: Trap set
- MDN: Reflect.set
- MDN: Reflect.get
- javascript.info: Proxy
Exercícios
- Exercício 1: Crie um Proxy que intercepte a leitura de qualquer propriedade e retorne uma string prefixada com "[DEBUG] ". Use
Reflect.getpara o comportamento padrão. - Exercício 2: Implemente um Proxy que valide a atribuição de uma propriedade
email. O valor deve conter um '@' e um '.'; caso contrário, lance umTypeError. UseReflect.set. - Exercício 3: Crie um Proxy que impeça a exclusão de propriedades que comecem com '_' (considere propriedades privadas). Use o trap
deleteProperty. - Exercício 4: Escreva um Proxy que conte quantas vezes uma função é chamada. Use o trap
apply. - Exercício 5: Crie um Proxy que torne todas as propriedades do objeto somente leitura. Ou seja, qualquer tentativa de atribuição deve ser ignorada (em modo não estrito) ou lançar erro (em modo estrito). Use o trap
setretornandofalse.
const alvo = { nome: "João", idade: 30 };
const handler = {
get(alvo, propriedade, receptor) {
return "[DEBUG] " + Reflect.get(alvo, propriedade, receptor);
}
};
const proxy = new Proxy(alvo, handler);
console.log(proxy.nome); // "[DEBUG] João"
console.log(proxy.idade); // "[DEBUG] 30"
const alvo = {};
const handler = {
set(alvo, propriedade, valor, receptor) {
if (propriedade === "email") {
if (!valor.includes("@") || !valor.includes(".")) {
throw new TypeError("Email inválido");
}
}
return Reflect.set(alvo, propriedade, valor, receptor);
}
};
const proxy = new Proxy(alvo, handler);
proxy.email = "usuario@example.com"; // OK
// proxy.email = "invalido"; // Lança TypeError
const alvo = { _privada: 1, publica: 2 };
const handler = {
deleteProperty(alvo, propriedade) {
if (propriedade.startsWith("_")) {
throw new Error("Não é possível excluir propriedade privada");
}
return Reflect.deleteProperty(alvo, propriedade);
}
};
const proxy = new Proxy(alvo, handler);
delete proxy.publica; // OK
// delete proxy._privada; // Lança erro
function soma(a, b) { return a + b; }
let contador = 0;
const handler = {
apply(alvo, thisArg, argumentos) {
contador++;
return Reflect.apply(alvo, thisArg, argumentos);
}
};
const proxySoma = new Proxy(soma, handler);
console.log(proxySoma(2, 3)); // 5
console.log(proxySoma(4, 5)); // 9
console.log(contador); // 2
const alvo = { a: 1, b: 2 };
const handler = {
set(alvo, propriedade, valor, receptor) {
// Retorna false para indicar que a atribuição falhou
return false;
}
};
const proxy = new Proxy(alvo, handler);
proxy.a = 10; // Em modo estrito, lança TypeError; em modo não estrito, falha silenciosamente
console.log(proxy.a); // 1 (inalterado)