Fundamentos de Classes em JavaScript
Classes em JavaScript são açúcar sintático construído sobre o modelo de protótipos, introduzidas na ES6 (2015). Diferentemente de linguagens clássicas, JavaScript usa protótipos internamente, mas as classes oferecem uma sintaxe familiar e mais legível para definir objetos e suas estruturas. Compreender essa abstração é essencial para trabalhar com código moderno e profissional.
Uma classe é um molde para criar objetos com propriedades e métodos. Sua sintaxe é direta: declare com class, defina o constructor para inicializar propriedades e crie métodos para comportamentos. Veja um exemplo prático:
class Veiculo {
constructor(marca, modelo) {
this.marca = marca;
this.modelo = modelo;
this.velocidade = 0;
}
acelerar(incremento) {
this.velocidade += incremento;
console.log(`${this.marca} ${this.modelo} acelerando para ${this.velocidade} km/h`);
}
frear() {
this.velocidade = 0;
console.log(`${this.marca} ${this.modelo} freado completamente`);
}
}
const carro = new Veiculo('Toyota', 'Corolla');
carro.acelerar(60);
carro.frear();
Métodos e Propriedades Estáticas
Métodos estáticos pertencem à classe, não às instâncias. Úteis para funcionalidades utilitárias que não dependem de dados de uma instância específica. Use a palavra-chave static:
class Calculadora {
static somar(a, b) {
return a + b;
}
static mediaAritmetica(numeros) {
const soma = numeros.reduce((acc, num) => acc + num, 0);
return soma / numeros.length;
}
}
console.log(Calculadora.somar(5, 3)); // 8
console.log(Calculadora.mediaAritmetica([10, 20, 30])); // 20
Herança e Composição
A herança permite que uma classe reutilize e estenda código de outra classe. Use extends para herdar e super() para acessar o construtor da classe pai. Este padrão é fundamental em arquiteturas orientadas a objeto, mas use com moderação — frequentemente composição é mais flexível que herança profunda.
class Veiculo {
constructor(marca) {
this.marca = marca;
}
info() {
return `Marca: ${this.marca}`;
}
}
class Carro extends Veiculo {
constructor(marca, portas) {
super(marca);
this.portas = portas;
}
info() {
return `${super.info()} | Portas: ${this.portas}`;
}
}
class Moto extends Veiculo {
constructor(marca, cilindrada) {
super(marca);
this.cilindrada = cilindrada;
}
info() {
return `${super.info()} | Cilindrada: ${this.cilindrada}cc`;
}
}
const carro = new Carro('Honda', 4);
const moto = new Moto('Yamaha', 600);
console.log(carro.info()); // Marca: Honda | Portas: 4
console.log(moto.info()); // Marca: Yamaha | Cilindrada: 600cc
Evitando Hierarquias Profundas
Em produção, evite heranças com mais de 2 ou 3 níveis. Prefira composição: injete comportamentos através de objetos passados ao construtor. Isso torna o código mais testável e flexível:
class MotorEletrico {
ligar() {
return 'Motor elétrico ligado silenciosamente';
}
}
class Bateria {
carga = 100;
descarregar() {
this.carga -= 10;
return `Bateria em ${this.carga}%`;
}
}
class VeiculoEletrico {
constructor(motor, bateria) {
this.motor = motor;
this.bateria = bateria;
}
ligarMotor() {
return this.motor.ligar();
}
usar() {
return this.bateria.descarregar();
}
}
const tesla = new VeiculoEletrico(new MotorEletrico(), new Bateria());
console.log(tesla.ligarMotor());
console.log(tesla.usar());
Encapsulamento: Privacidade e Controle
O encapsulamento protege dados internos de acesso direto, expondo apenas o necessário através de uma interface pública. Em JavaScript moderno, propriedades privadas usam o prefixo #. Isso garante que apenas métodos internos à classe possam acessá-las, prevenindo modificações acidentais e facilitando refatoração segura em produção.
class ContaBancaria {
#saldo = 0;
#historico = [];
constructor(titular, saldoInicial = 0) {
this.titular = titular;
this.#saldo = saldoInicial;
this.#historico.push({ operacao: 'Abertura', valor: saldoInicial, data: new Date() });
}
depositar(valor) {
if (valor <= 0) {
throw new Error('Valor deve ser positivo');
}
this.#saldo += valor;
this.#historico.push({ operacao: 'Depósito', valor, data: new Date() });
return `Depósito de R$${valor} realizado. Saldo: R$${this.#saldo}`;
}
sacar(valor) {
if (valor > this.#saldo) {
throw new Error('Saldo insuficiente');
}
this.#saldo -= valor;
this.#historico.push({ operacao: 'Saque', valor, data: new Date() });
return `Saque de R$${valor} realizado. Saldo: R$${this.#saldo}`;
}
obterSaldo() {
return this.#saldo;
}
obterHistorico() {
return [...this.#historico];
}
}
const conta = new ContaBancaria('João', 1000);
console.log(conta.depositar(500));
console.log(conta.sacar(200));
console.log(conta.obterSaldo());
// conta.#saldo = -9999; // Erro: propriedade privada é inacessível
Getters e Setters
Getters e setters permitem validação e lógica customizada ao acessar propriedades, mantendo a sintaxe de propriedades simples. Essencial para classes em produção que precisam de regras de negócio:
class Usuario {
#email;
#idade;
constructor(nome, email, idade) {
this.nome = nome;
this.email = email;
this.idade = idade;
}
get email() {
return this.#email;
}
set email(novoEmail) {
if (!novoEmail.includes('@')) {
throw new Error('Email inválido');
}
this.#email = novoEmail;
}
get idade() {
return this.#idade;
}
set idade(novaIdade) {
if (novaIdade < 0 || novaIdade > 150) {
throw new Error('Idade inválida');
}
this.#idade = novaIdade;
}
}
const usuario = new Usuario('Maria', 'maria@email.com', 28);
console.log(usuario.email);
usuario.idade = 29;
console.log(usuario.idade);
// usuario.email = 'invalido'; // Erro: Email inválido
Boas Práticas em Produção
Em ambientes reais, siga estas diretrizes: (1) Use encapsulamento por padrão — propriedades privadas protegem sua arquitetura; (2) Prefira composição a herança profunda — é mais simples de entender e testar; (3) Valide dados nos setters e construtores — evita estados inválidos; (4) Documente com JSDoc — facilita manutenção futura.
/**
* Classe que representa um Produto no sistema de e-commerce
* @class Produto
* @param {string} nome - Nome do produto
* @param {number} preco - Preço em reais
* @param {number} estoque - Quantidade em estoque
*/
class Produto {
#preco;
#estoque;
constructor(nome, preco, estoque) {
this.nome = nome;
this.preco = preco;
this.estoque = estoque;
}
get preco() {
return this.#preco;
}
set preco(valor) {
if (valor <= 0) throw new Error('Preço deve ser positivo');
this.#preco = valor;
}
/**
* Reduz estoque após venda
* @param {number} quantidade
* @returns {boolean} Sucesso da operação
*/
vender(quantidade) {
if (quantidade > this.#estoque) return false;
this.#estoque -= quantidade;
return true;
}
obterEstoque() {
return this.#estoque;
}
}
const produto = new Produto('Notebook', 3000, 5);
produto.vender(2);
console.log(produto.obterEstoque()); // 3
Conclusão
Classes em JavaScript oferecem uma abordagem profissional e estruturada para organizar código. O domínio de herança com extends e super, encapsulamento com propriedades privadas e getters/setters com validação é indispensável em projetos reais. Lembre-se: prefira composição quando possível, sempre valide dados e documente sua código — essas práticas garantem manutenibilidade a longo prazo.