Getters, setters e propriedades computadas
Nesta aula, você aprenderá a criar getters e setters em JavaScript, tanto em classes quanto em objetos literais, e a usar Object.defineProperty para definir propriedades computadas. Veremos casos de uso práticos como validação de dados, cálculos automáticos e encapsulamento, com exemplos de código e exercícios para fixação.
Getters e setters são recursos poderosos do JavaScript que permitem controlar o acesso a propriedades de um objeto. Eles são métodos especiais que são chamados quando você lê (get) ou escreve (set) uma propriedade, permitindo executar lógica adicional, validar dados ou calcular valores dinamicamente. Nesta aula, vamos explorar como definir acessores em objetos literais e em classes, além de usar Object.defineProperty para propriedades computadas mais avançadas.
Esses conceitos são fundamentais para criar código mais limpo, seguro e expressivo, especialmente em aplicações que envolvem modelagem de dados, validação de entrada e encapsulamento. Vamos mergulhar nos detalhes e ver exemplos práticos que você pode aplicar no seu dia a dia.
Definindo acessores
Em JavaScript, existem duas maneiras principais de definir getters e setters: em objetos literais e em classes. Ambas usam as palavras-chave get e set.
Em um objeto literal, você pode definir um getter e um setter para uma propriedade, como no exemplo abaixo. O getter é uma função que retorna o valor da propriedade, e o setter é uma função que recebe o novo valor e pode processá-lo antes de armazená-lo.
const pessoa = {
_nome: 'João',
get nome() {
return this._nome;
},
set nome(novoNome) {
if (novoNome.length > 0) {
this._nome = novoNome;
} else {
console.error('Nome inválido');
}
}
};
console.log(pessoa.nome); // João
pessoa.nome = 'Maria';
console.log(pessoa.nome); // Maria
pessoa.nome = ''; // Nome inválido
No exemplo, usamos uma propriedade interna _nome (com underscore) para armazenar o valor real, enquanto o getter e o setter controlam o acesso. Isso é uma convenção comum para evitar conflitos de nomenclatura.
Em classes, a sintaxe é semelhante. Vamos criar uma classe ContaBancaria com um getter e setter para o saldo, adicionando validação:
class ContaBancaria {
constructor(titular) {
this._titular = titular;
this._saldo = 0;
}
get saldo() {
return this._saldo;
}
set saldo(valor) {
if (valor < 0) {
throw new Error('Saldo não pode ser negativo');
}
this._saldo = valor;
}
depositar(valor) {
this._saldo += valor;
}
sacar(valor) {
if (valor > this._saldo) {
throw new Error('Saldo insuficiente');
}
this._saldo -= valor;
}
}
const conta = new ContaBancaria('Ana');
conta.depositar(100);
console.log(conta.saldo); // 100
conta.saldo = 200;
console.log(conta.saldo); // 200
Observe que o setter valida se o valor é negativo, lançando uma exceção. Isso garante que o saldo nunca fique inválido, mesmo se alguém tentar atribuir diretamente.
Object.defineProperty
Outra forma de definir getters e setters é usando o método estático Object.defineProperty. Esse método permite definir propriedades com descritores, incluindo get e set, e é útil quando você precisa criar propriedades dinamicamente ou em objetos que não são literais.
A sintaxe é: Object.defineProperty(obj, prop, descriptor), onde descriptor é um objeto que pode conter get, set, enumerable, configurable, entre outros. Veja um exemplo:
const produto = {};
Object.defineProperty(produto, 'preco', {
get() {
return this._preco;
},
set(valor) {
if (valor < 0) {
throw new Error('Preço inválido');
}
this._preco = valor;
},
enumerable: true,
configurable: true
});
produto.preco = 19.99;
console.log(produto.preco); // 19.99
produto.preco = -5; // Erro: Preço inválido
Uma vantagem de Object.defineProperty é que você pode definir várias propriedades de uma vez usando Object.defineProperties. Também é possível definir propriedades em objetos que já existem, o que é útil para adicionar comportamentos a objetos de bibliotecas ou dados vindos de APIs.
Outra aplicação é criar propriedades computadas, que calculam um valor a partir de outras propriedades. Por exemplo:
const pessoa = {};
Object.defineProperties(pessoa, {
primeiroNome: { value: 'João', writable: true },
sobrenome: { value: 'Silva', writable: true },
nomeCompleto: {
get() {
return `${this.primeiroNome} ${this.sobrenome}`;
}
}
});
console.log(pessoa.nomeCompleto); // João Silva
pessoa.primeiroNome = 'Maria';
console.log(pessoa.nomeCompleto); // Maria Silva
Nesse caso, nomeCompleto é uma propriedade computada que depende de primeiroNome e sobrenome. Ela não armazena um valor próprio, mas calcula na hora do acesso.
Casos de uso
Getters e setters são amplamente utilizados em situações que exigem controle sobre os dados. Vamos ver alguns casos comuns:
- Validação de dados: como nos exemplos acima, os setters podem validar valores antes de armazená-los, garantindo que o objeto permaneça em um estado consistente.
- Encapsulamento: você pode expor uma interface pública enquanto mantém detalhes de implementação privados, usando propriedades internas com underscore ou símbolos.
- Cálculos automáticos: propriedades computadas, como área de um círculo ou nome completo, podem ser calculadas dinamicamente, evitando armazenamento redundante.
- Log e auditoria: os setters podem registrar alterações, por exemplo, para fins de depuração ou histórico.
- Compatibilidade: em bibliotecas, getters e setters podem ser usados para migrar de propriedades simples para propriedades com lógica sem quebrar o código do usuário.
Abaixo, um exemplo mais complexo que combina validação e cálculo:
class Retangulo {
constructor(largura, altura) {
this._largura = 0;
this._altura = 0;
this.largura = largura;
this.altura = altura;
}
get largura() {
return this._largura;
}
set largura(valor) {
if (valor <= 0) {
throw new Error('Largura deve ser positiva');
}
this._largura = valor;
}
get altura() {
return this._altura;
}
set altura(valor) {
if (valor <= 0) {
throw new Error('Altura deve ser positiva');
}
this._altura = valor;
}
get area() {
return this._largura * this._altura;
}
}
const ret = new Retangulo(5, 10);
console.log(ret.area); // 50
ret.largura = 7;
console.log(ret.area); // 70
Esse exemplo mostra como os getters podem ser usados para expor propriedades calculadas (área) sem a necessidade de armazená-las, mantendo o objeto enxuto.
Boas práticas
- Use
_como prefixo em propriedades internas para indicar que não devem ser acessadas diretamente. - Prefira getters e setters em classes para encapsulamento, mas lembre-se de que eles não são privados de verdade (a convenção é apenas visual).
- Evite lógica complexa dentro de getters, pois eles podem ser chamados muitas vezes, impactando a performance.
- Use
Object.definePropertyquando precisar definir propriedades em objetos que não são classes, ou quando precisar de controle fino sobre enumerabilidade e configurabilidade.
Referências
- MDN: get
- MDN: set
- MDN: Object.defineProperty
- MDN: Object.defineProperties
- MDN: Classes
- JavaScript.info: Property getters and setters
- W3Schools: JavaScript Object Accessors
Exercícios
Crie um objeto
temperaturacom propriedades internas_celsiuse_fahrenheit. Defina getters e setters paracelsiusefahrenheitque convertam automaticamente entre as unidades. Por exemplo, se você definircelsiuspara 100,fahrenheitdeve retornar 212.✓ Resposta:const temperatura = { _celsius: 0, get celsius() { return this._celsius; }, set celsius(valor) { this._celsius = valor; this._fahrenheit = (valor * 9/5) + 32; }, get fahrenheit() { return this._fahrenheit; }, set fahrenheit(valor) { this._fahrenheit = valor; this._celsius = (valor - 32) * 5/9; } }; temperatura.celsius = 100; console.log(temperatura.fahrenheit); // 212Usando
Object.defineProperty, crie um objetopessoacom uma propriedadeidadeque valide se o valor é um número entre 0 e 120 (inclusive). Se for inválido, lance um erro.✓ Resposta:const pessoa = {}; Object.defineProperty(pessoa, 'idade', { get() { return this._idade; }, set(valor) { if (typeof valor !== 'number' || valor < 0 || valor > 120) { throw new Error('Idade inválida'); } this._idade = valor; } }); pessoa.idade = 30; console.log(pessoa.idade); // 30 pessoa.idade = 150; // ErroCrie uma classe
Pessoacom propriedadesnomeesobrenome(privadas) e um getternomeCompletoque retorna a concatenação. Também adicione um setternomeCompletoque divide o valor em nome e sobrenome (pelo primeiro espaço).✓ Resposta:class Pessoa { constructor(nome, sobrenome) { this._nome = nome; this._sobrenome = sobrenome; } get nomeCompleto() { return `${this._nome} ${this._sobrenome}`; } set nomeCompleto(valor) { const partes = valor.split(' '); this._nome = partes[0]; this._sobrenome = partes.slice(1).join(' '); } } const pessoa = new Pessoa('João', 'Silva'); console.log(pessoa.nomeCompleto); // João Silva pessoa.nomeCompleto = 'Maria Souza'; console.log(pessoa._nome); // Maria console.log(pessoa._sobrenome); // SouzaUsando
Object.defineProperties, crie um objetocarrinhocom uma propriedadeitens(array) e um gettertotalque calcula a soma dos preços dos itens. Cada item deve terpreco.✓ Resposta:const carrinho = {}; Object.defineProperties(carrinho, { itens: { value: [], writable: true, enumerable: true }, total: { get() { return this.itens.reduce((soma, item) => soma + item.preco, 0); } } }); carrinho.itens.push({ nome: 'Camiseta', preco: 29.99 }); carrinho.itens.push({ nome: 'Calça', preco: 79.99 }); console.log(carrinho.total); // 109.98Explique a diferença entre usar getters/setters em um objeto literal e usar
Object.defineProperty. Dê um exemplo de quando você escolheria um sobre o outro.✓ Resposta: A principal diferença é que em objetos literais a sintaxe é mais concisa e direta, enquantoObject.definePropertyoferece controle adicional sobre as características da propriedade (enumerable, configurable, etc.) e permite definir propriedades em objetos existentes. Você usariaObject.definePropertyquando precisa definir propriedades dinamicamente, quando quer configurar atributos comoenumerableouconfigurable, ou quando está trabalhando com objetos que não são literais (ex.: criados por uma função construtora). Objetos literais são mais adequados para casos simples onde você está definindo o objeto de uma só vez.
Observações finais
Getters e setters são ferramentas essenciais no JavaScript moderno. Eles permitem que você crie APIs mais limpas e seguras, escondendo a complexidade interna. Lembre-se de que, apesar da convenção _, as propriedades ainda são acessíveis diretamente; para encapsulamento real, você pode usar # (campos privados) em classes, que veremos em aulas futuras. Pratique os exercícios e experimente criar seus próprios exemplos para dominar esse recurso.