Object.defineProperty: Controle Fino sobre Propriedades
Object.defineProperty é o alicerce da metaprogramação em JavaScript. Ele permite definir propriedades de objetos com controle granular sobre comportamentos como leitura, escrita e enumeração. Diferentemente da atribuição simples, este método possibilita criar getters, setters e descriptores que interceptam operações.
const usuario = {};
Object.defineProperty(usuario, 'nome', {
value: 'João',
writable: false, // impede alteração
enumerable: true, // aparece em for...in
configurable: false // impede redefinição
});
usuario.nome; // 'João'
usuario.nome = 'Maria'; // silencioso em modo não-strict
console.log(usuario.nome); // 'João'
// Com getter e setter
Object.defineProperty(usuario, 'idade', {
get() {
return this._idade || 0;
},
set(value) {
if (value < 0) throw new Error('Idade inválida');
this._idade = value;
},
enumerable: true,
configurable: true
});
usuario.idade = 25; // ativa o setter
console.log(usuario.idade); // 25
Esse controle é fundamental para validação, reatividade e proteção de dados. Frameworks como Vue.js usam getters/setters para detectar mudanças e atualizar a interface automaticamente.
Validação e Proteção de Dados com Descriptores
Combinando Object.defineProperty com lógica de negócio, criamos camadas de proteção robustas. Um caso prático é forçar validação antes de armazenar valores ou permitir apenas leitura de certas propriedades.
class Produto {
constructor(nome, preco) {
this._preco = preco;
Object.defineProperty(this, 'preco', {
get() {
return this._preco;
},
set(valor) {
if (typeof valor !== 'number' || valor < 0) {
throw new TypeError('Preço deve ser um número positivo');
}
console.log(`Preço atualizado: R$ ${valor}`);
this._preco = valor;
},
enumerable: true
});
Object.defineProperty(this, 'nome', {
value: nome,
writable: false,
enumerable: true
});
}
}
const produto = new Produto('Notebook', 2500);
produto.preco = 2800; // Preço atualizado: R$ 2800
produto.preco = -100; // TypeError: Preço deve ser um número positivo
produto.nome = 'Desktop'; // silencioso, não altera
Essa abordagem é essencial em aplicações financeiras, sistemas de permissões e validação de dados em tempo real.
Decorators: Sintaxe Moderna para Metaprogramação
Decorators são uma sintaxe elegante (ainda experimental em JavaScript, mas já padrão em TypeScript) que encapsula a lógica de Object.defineProperty e reflection. Um decorator é uma função que modifica o comportamento de classes, métodos ou propriedades.
// Decorator simples para logging
function log(target, propertyKey, descriptor) {
const metodoOriginal = descriptor.value;
descriptor.value = function(...args) {
console.log(`Chamando ${propertyKey} com args:`, args);
const resultado = metodoOriginal.apply(this, args);
console.log(`${propertyKey} retornou:`, resultado);
return resultado;
};
return descriptor;
}
class Calculadora {
@log
somar(a, b) {
return a + b;
}
}
const calc = new Calculadora();
calc.somar(5, 3);
// Chamando somar com args: [5, 3]
// somar retornou: 8
Para usar decorators no JavaScript padrão (sem TypeScript), você precisará de um transpilador. Mas a ideia é poderosa: separar a lógica de negócio da infraestrutura.
Decorators Reutilizáveis
// Decorator para cachear resultados
function cache(target, propertyKey, descriptor) {
const metodo = descriptor.value;
const cache = new Map();
descriptor.value = function(...args) {
const chave = JSON.stringify(args);
if (cache.has(chave)) {
console.log('Retornando do cache');
return cache.get(chave);
}
const resultado = metodo.apply(this, args);
cache.set(chave, resultado);
return resultado;
};
return descriptor;
}
class Fibonacci {
@cache
calcular(n) {
console.log(`Calculando fib(${n})`);
if (n <= 1) return n;
return this.calcular(n - 1) + this.calcular(n - 2);
}
}
const fib = new Fibonacci();
fib.calcular(5);
fib.calcular(5); // Retornando do cache
Decorators são particularmente úteis em autenticação, validação, auditoria e transformação de dados de forma declarativa.
Reflection e Introspeção Avançada
A verdadeira potência da metaprogramação emerge quando combinamos Object.defineProperty com Reflect API para introspecção. Isso permite inspecionar e modificar estruturas em tempo de execução com segurança.
class Usuario {
constructor(dados) {
// Cria propriedades reativas
Object.keys(dados).forEach(chave => {
Object.defineProperty(this, chave, {
get() {
return this[`_${chave}`];
},
set(valor) {
if (Reflect.has(this, `_${chave}`)) {
console.log(`Campo ${chave} alterado para ${valor}`);
}
this[`_${chave}`] = valor;
},
enumerable: true
});
});
// Popula valores iniciais
Object.assign(this, dados);
}
toJSON() {
return Object.getOwnPropertyNames(this)
.filter(p => !p.startsWith('_'))
.reduce((obj, chave) => {
obj[chave] = this[chave];
return obj;
}, {});
}
}
const user = new Usuario({ email: 'teste@mail.com', ativo: true });
user.email = 'novo@mail.com'; // Campo email alterado para novo@mail.com
console.log(JSON.stringify(user)); // {"email":"novo@mail.com","ativo":true}
Este padrão é fundamental em frameworks modernos para reatividade automática e serialização inteligente de dados.
Conclusão
Metaprogramação em JavaScript, através de Object.defineProperty e Decorators, permite criar código mais elegante, seguro e manutenível. Primeiro, Object.defineProperty fornece controle preciso sobre propriedades, essencial para validação e proteção de dados. Segundo, Decorators oferecem uma sintaxe limpa e reutilizável para separar concerns de infraestrutura da lógica de negócio. Terceiro, combinados com Reflect, esses mecanismos possibilitam frameworks reativos e sistemas de auditoria sofisticados—compreendê-los é fundamental para dominar JavaScript moderno.