Getters, setters e propriedades computadas
Nesta aula, você aprenderá a usar getters e setters em JavaScript para controlar o acesso a propriedades de objetos, além de explorar a criação de propriedades computadas e a técnica com Object.defineProperty. Vamos entender os casos de uso práticos, como validação, encapsulamento e cálculo automático, com exemplos claros e exercícios.
Getters e setters são recursos poderosos do JavaScript que permitem definir o comportamento de acesso e atribuição de propriedades de um objeto. Em vez de simplesmente armazenar um valor, você pode executar lógica personalizada quando uma propriedade é lida ou modificada. Isso é fundamental para o encapsulamento, validação de dados e criação de propriedades computadas, que são calculadas dinamicamente a partir de outros valores.
Nesta aula, vamos explorar como definir getters e setters em classes e objetos literais, como usar Object.defineProperty para um controle mais fino, e quais são os casos de uso mais comuns, como validação, normalização de dados e propriedades derivadas. Ao final, você terá uma compreensão sólida para aplicar esses conceitos em seus projetos.
Definindo acessores
Em JavaScript, getters e setters podem ser definidos em objetos literais e em classes. A sintaxe é simples: usamos as palavras-chave get e set antes do nome da propriedade. Um getter é uma função que é chamada quando a propriedade é lida, e um setter é chamado quando a propriedade é atribuída. Ambos são opcionais: você pode ter apenas um getter (propriedade somente leitura) ou apenas um setter (raro, mas possível).
Exemplo com objeto literal:
const pessoa = {
_nome: 'João',
get nome() {
return this._nome;
},
set nome(valor) {
if (valor.length < 3) {
throw new Error('Nome muito curto');
}
this._nome = valor;
}
};
console.log(pessoa.nome); // 'João'
pessoa.nome = 'Maria';
console.log(pessoa.nome); // 'Maria'
try {
pessoa.nome = 'Al';
} catch (e) {
console.log(e.message); // 'Nome muito curto'
}Observe que usamos uma convenção de prefixo com underscore (_nome) para armazenar o valor real, pois o getter e o setter não podem ter o mesmo nome da propriedade que armazenam. Isso evita recursão infinita.
Em classes, a sintaxe é semelhante:
class ContaBancaria {
constructor(saldoInicial = 0) {
this._saldo = saldoInicial;
}
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(100);
console.log(conta.saldo); // 100
conta.depositar(50);
console.log(conta.saldo); // 150
conta.saldo = 200;
console.log(conta.saldo); // 200
try {
conta.saldo = -10;
} catch (e) {
console.log(e.message); // 'Saldo não pode ser negativo'
}Os getters e setters são especialmente úteis para validar dados antes de armazená-los, garantindo que o objeto permaneça em um estado consistente.
Object.defineProperty
Além da sintaxe direta, podemos usar o método estático Object.defineProperty para definir getters e setters em qualquer objeto, mesmo que já existente. Isso é útil quando você não pode alterar a definição original do objeto ou precisa definir propriedades com atributos específicos (como enumerable, configurable e writable). A sintaxe é:
Object.defineProperty(obj, 'nomeDaPropriedade', {
get() { ... },
set(valor) { ... },
enumerable: true,
configurable: true
});Exemplo:
const carro = {};
let _velocidade = 0;
Object.defineProperty(carro, 'velocidade', {
get() {
return _velocidade;
},
set(novaVelocidade) {
if (novaVelocidade < 0 || novaVelocidade > 200) {
throw new Error('Velocidade inválida');
}
_velocidade = novaVelocidade;
},
enumerable: true,
configurable: true
});
carro.velocidade = 100;
console.log(carro.velocidade); // 100
carro.velocidade = 250; // Erro: Velocidade inválidaUma vantagem do Object.defineProperty é que ele permite criar propriedades computadas que dependem de outras propriedades do objeto. Por exemplo, podemos definir uma propriedade area que calcula a área de um retângulo a partir de largura e altura:
const retangulo = { largura: 10, altura: 5 };
Object.defineProperty(retangulo, 'area', {
get() {
return this.largura * this.altura;
},
enumerable: true
});
console.log(retangulo.area); // 50
retangulo.largura = 20;
console.log(retangulo.area); // 100Note que a propriedade area é computada automaticamente sempre que é lida, refletindo as mudanças nas dimensões.
Casos de uso
Os getters e setters são amplamente utilizados em várias situações:
- Validação de dados: Garantir que valores atribuídos atendam a certos critérios (ex.: idade positiva, nome não vazio).
- Encapsulamento: Ocultar a implementação interna e expor uma interface controlada, como no caso de saldo bancário.
- Propriedades computadas: Derivar valores automaticamente de outras propriedades (ex.: área, nome completo).
- Normalização: Transformar valores ao atribuir, como converter para maiúsculas ou trim.
- Lazy loading: Calcular um valor apenas quando necessário e armazenar em cache.
Um exemplo prático de propriedade computada é a criação de um nome completo a partir de nome e sobrenome:
class Usuario {
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 user = new Usuario('João', 'Silva');
console.log(user.nomeCompleto); // 'João Silva'
user.nomeCompleto = 'Maria Santos Oliveira';
console.log(user.nome); // 'Maria'
console.log(user.sobrenome); // 'Santos Oliveira'Outro caso comum é a normalização de dados, como garantir que um email seja sempre armazenado em minúsculas:
class Contato {
constructor(email) {
this._email = email.toLowerCase();
}
get email() {
return this._email;
}
set email(valor) {
this._email = valor.toLowerCase().trim();
}
}
const contato = new Contato('Joao@Email.com');
console.log(contato.email); // 'joao@email.com'Em frameworks como Vue.js, getters e setters são usados para reatividade, mas em JavaScript puro, eles são essenciais para criar APIs limpas e seguras.
Boas práticas
- Use underscore (
_) como convenção para propriedades privadas que serão acessadas por getters/setters. - Evite getters que tenham efeitos colaterais; eles devem apenas retornar um valor.
- Setters devem validar dados e lançar erros quando necessário.
- Prefira getters para propriedades computadas que dependem de outras propriedades.
- Use
Object.definePropertyquando precisar definir propriedades com atributos específicos ou em objetos que não podem ser alterados diretamente.
Referências
- MDN: get
- MDN: set
- MDN: Object.defineProperty
- javascript.info: Property getters and setters
- W3Schools: JavaScript Object Accessors
- MDN: Working with Objects
Exercícios
- Exercício 1: Crie um objeto literal chamado
termometrocom um gettercelsiuse um settercelsius. O setter deve aceitar apenas valores entre -100 e 100. Adicione também um getterfahrenheitque retorna a temperatura convertida (F = C * 9/5 + 32). Teste atribuindo valores válidos e inválidos.✓ Resposta:const termometro = { _celsius: 0, get celsius() { return this._celsius; }, set celsius(valor) { if (valor < -100 || valor > 100) { throw new Error('Temperatura fora do intervalo permitido'); } this._celsius = valor; }, get fahrenheit() { return this._celsius * 9 / 5 + 32; } }; termometro.celsius = 25; console.log(termometro.celsius); // 25 console.log(termometro.fahrenheit); // 77 try { termometro.celsius = 200; } catch (e) { console.log(e.message); // 'Temperatura fora do intervalo permitido' } - Exercício 2: Defina uma classe
Pessoacom propriedadesnomeeidade. Adicione um gettermaiorDeIdadeque retornatruese a idade for maior ou igual a 18, e um setteridadeque valida se o valor é um número positivo e menor que 150. Crie um objeto e teste.✓ Resposta:class Pessoa { constructor(nome, idade) { this.nome = nome; this.idade = idade; } get idade() { return this._idade; } set idade(valor) { if (!Number.isInteger(valor) || valor < 0 || valor > 150) { throw new Error('Idade inválida'); } this._idade = valor; } get maiorDeIdade() { return this._idade >= 18; } } const pessoa = new Pessoa('João', 20); console.log(pessoa.maiorDeIdade); // true try { pessoa.idade = -5; } catch (e) { console.log(e.message); // 'Idade inválida' } - Exercício 3: Use
Object.definePropertypara criar uma propriedadeareaem um objetocirculoque tenharaio. O getter deve calcular a área (π * raio²). Faça com que a propriedaderaioseja enumerável e configurável.✓ Resposta:const circulo = { raio: 5 }; Object.defineProperty(circulo, 'area', { get() { return Math.PI * this.raio ** 2; }, enumerable: true, configurable: true }); console.log(circulo.area); // 78.53981633974483 circulo.raio = 10; console.log(circulo.area); // 314.1592653589793 - Exercício 4: Crie uma classe
ContaCorrentecom um saldo privado (_saldo) e um limite de cheque especial. O gettersaldoDisponiveldeve retornar_saldo+ limite. O settersaldodeve permitir que o saldo fique negativo até o limite negativo, mas não além. Teste com depósitos e saques.✓ Resposta:class ContaCorrente { constructor(saldoInicial, limite) { this._saldo = saldoInicial; this.limite = limite; } get saldo() { return this._saldo; } set saldo(valor) { if (valor < -this.limite) { throw new Error('Saldo ultrapassa o limite do cheque especial'); } this._saldo = valor; } get saldoDisponivel() { return this._saldo + this.limite; } depositar(valor) { this._saldo += valor; } sacar(valor) { if (valor > this.saldoDisponivel) { throw new Error('Saldo insuficiente'); } this._saldo -= valor; } } const conta = new ContaCorrente(100, 200); console.log(conta.saldoDisponivel); // 300 conta.sacar(250); console.log(conta.saldo); // -150 console.log(conta.saldoDisponivel); // 50 try { conta.sacar(100); // saldo ficaria -250, abaixo do limite -200 } catch (e) { console.log(e.message); // 'Saldo insuficiente' } - Exercício 5: Crie um objeto
temperaturacom getters e setters para Celsius e Fahrenheit, mas armazene internamente apenas em Celsius. O setter de Fahrenheit deve converter para Celsius antes de armazenar. UseObject.definePropertypara definir ambos.✓ Resposta:const temperatura = { _celsius: 0 }; Object.defineProperty(temperatura, 'celsius', { get() { return this._celsius; }, set(valor) { this._celsius = valor; }, enumerable: true, configurable: true }); Object.defineProperty(temperatura, 'fahrenheit', { get() { return this._celsius * 9 / 5 + 32; }, set(valor) { this._celsius = (valor - 32) * 5 / 9; }, enumerable: true, configurable: true }); temperatura.celsius = 25; console.log(temperatura.fahrenheit); // 77 temperatura.fahrenheit = 100; console.log(temperatura.celsius); // 37.77777777777778