Classes em JavaScript
Nesta aula, você aprenderá a criar e usar classes em JavaScript, incluindo a sintaxe moderna, construtores, métodos, campos de classe e a palavra-chave 'this'. Exploraremos exemplos práticos e boas práticas para dominar a programação orientada a objetos em JS.
As classes em JavaScript, introduzidas no ES6, fornecem uma sintaxe mais clara e familiar para criar objetos e lidar com herança, baseada em protótipos. Elas são um 'açúcar sintático' sobre o sistema de protótipos existente, mas trazem recursos como construtores, métodos, campos e encapsulamento. Nesta aula, vamos explorar a fundo cada parte da definição de uma classe, desde a sintaxe básica até o uso correto de this.
Entender classes é essencial para organizar código em paradigma orientado a objetos, facilitando a reutilização, manutenção e escalabilidade. Vamos começar com a estrutura básica e depois avançar para recursos mais avançados, sempre com exemplos práticos.
Sintaxe
A sintaxe de uma classe em JavaScript é semelhante a outras linguagens como Java ou C#, mas com suas particularidades. Uma classe é declarada com a palavra-chave class seguida do nome da classe (por convenção, com letra maiúscula). O corpo da classe contém métodos, construtor e campos. Diferente de funções construtoras, as classes não são 'hoisted' (elevadas), então devem ser declaradas antes de serem usadas.
Abaixo, um exemplo mínimo de uma classe:
class Pessoa {
constructor(nome) {
this.nome = nome;
}
saudacao() {
console.log(`Olá, meu nome é ${this.nome}`);
}
}
const p = new Pessoa('Maria');
p.saudacao(); // "Olá, meu nome é Maria"Observe que a classe é definida com class, o construtor é um método especial chamado constructor e os métodos são definidos sem a palavra-chave function. A instância é criada com new. Também é possível usar expressões de classe (atribuir a classe a uma variável) e classes anônimas.
Construtor
O construtor é um método especial que é executado quando uma nova instância é criada com new. Ele é usado para inicializar propriedades do objeto. Cada classe pode ter apenas um construtor. Se não for definido, um construtor padrão vazio é usado.
O construtor pode receber parâmetros e definir propriedades usando this. Veja um exemplo com validação:
class Produto {
constructor(nome, preco) {
this.nome = nome;
if (preco < 0) {
throw new Error('Preço não pode ser negativo');
}
this.preco = preco;
}
}
const camiseta = new Produto('Camiseta', 29.90);
console.log(camiseta); // Produto { nome: 'Camiseta', preco: 29.9 }O construtor também pode chamar o construtor de uma classe pai usando super() (veremos em herança, mas é importante saber). Além disso, é possível retornar um objeto explícito do construtor, mas isso é raro e pode quebrar a semântica esperada.
Métodos
Métodos são funções definidas dentro da classe que operam nos dados da instância. Eles são definidos sem a palavra-chave function e são adicionados ao protótipo da classe, o que significa que são compartilhados entre todas as instâncias, economizando memória.
Além dos métodos de instância, existem métodos estáticos (usando static) que são chamados na própria classe, não na instância. Eles são úteis para funções utilitárias relacionadas à classe. Veja um exemplo:
class Calculadora {
static soma(a, b) {
return a + b;
}
multiplica(a, b) {
return a * b;
}
}
console.log(Calculadora.soma(2, 3)); // 5
const calc = new Calculadora();
console.log(calc.multiplica(2, 3)); // 6Métodos podem ter nomes computados, getters e setters, e também podem ser assíncronos. Getters e setters são definidos com as palavras-chave get e set e permitem controlar o acesso a propriedades.
Campos
Campos (ou propriedades de classe) são variáveis que pertencem à instância. Antes do ES2022, era comum definir campos dentro do construtor, mas agora temos a sintaxe de campos de classe, que permite declará-los diretamente no corpo da classe. Eles podem ser inicializados com valores padrão.
Existem campos públicos (acessíveis externamente) e campos privados (com # antes do nome, que só podem ser acessados dentro da classe). Veja um exemplo:
class ContaBancaria {
// Campos públicos
titular;
saldo = 0;
// Campo privado
#senha;
constructor(titular, senha) {
this.titular = titular;
this.#senha = senha;
}
depositar(valor) {
if (valor > 0) {
this.saldo += valor;
}
}
verificarSenha(senha) {
return senha === this.#senha;
}
}
const conta = new ContaBancaria('João', '1234');
conta.depositar(100);
console.log(conta.saldo); // 100
console.log(conta.verificarSenha('1234')); // true
// conta.#senha; // Erro: SyntaxErrorCampos privados são uma adição recente e garantem encapsulamento real. Eles não são acessíveis fora da classe, nem mesmo por herança. Isso é uma melhoria significativa em relação a convenções como underscore (_prop).
this
A palavra-chave this em JavaScript é um dos conceitos mais confusos, mas em classes ela se comporta de forma mais previsível. Dentro de um método de instância, this se refere à instância da classe. No construtor, this também se refere à nova instância que está sendo criada.
No entanto, this pode mudar de contexto se o método for passado como callback ou extraído. Por exemplo:
class Pessoa {
constructor(nome) {
this.nome = nome;
}
dizerOla() {
console.log(`Olá, eu sou ${this.nome}`);
}
}
const p = new Pessoa('Ana');
const fn = p.dizerOla;
fn(); // Erro: Cannot read properties of undefined (reading 'nome')
// Porque this não é mais a instância pPara contornar isso, podemos usar bind, arrow functions (que não têm seu próprio this e capturam o contexto léxico), ou métodos de classe como arrow functions (proposta recente). Uma prática comum é usar arrow functions para métodos que precisam manter o contexto, mas isso tem trade-offs (não são prototipados). Veja como corrigir:
class Pessoa {
constructor(nome) {
this.nome = nome;
// bind no construtor
this.dizerOla = this.dizerOla.bind(this);
}
dizerOla() {
console.log(`Olá, eu sou ${this.nome}`);
}
}
// Ou usar arrow function como campo:
class Pessoa2 {
constructor(nome) {
this.nome = nome;
}
dizerOla = () => {
console.log(`Olá, eu sou ${this.nome}`);
};
}
const p = new Pessoa2('Ana');
const fn = p.dizerOla;
fn(); // FuncionaÉ importante entender como this funciona para evitar bugs sutis. Em métodos estáticos, this se refere à própria classe.
Boas Práticas e Observações Finais
Ao usar classes em JavaScript, algumas boas práticas incluem: sempre usar constructor para inicializar propriedades (ou campos de classe com valores padrão); prefira campos privados para dados sensíveis; evite métodos de instância com arrow functions se você precisar de herança, pois eles não são adicionados ao protótipo; use super() corretamente em subclasses; e lembre-se de que classes são 'strict mode' por padrão, então não pode esquecer declarações de variáveis.
Além disso, considere que classes são uma ferramenta poderosa, mas não são a única forma de orientação a objetos em JavaScript. Em projetos mais simples, funções fábrica podem ser suficientes. Avalie o contexto.
Exercícios
- Exercício 1: Crie uma classe
Livrocom construtor que recebatituloeautor. Adicione um métododescricao()que retorna uma string no formato "Título: X, Autor: Y". - Exercício 2: Defina uma classe
Temperaturacom um campo privado#celsiuse métodossetCelsius(valor)egetFahrenheit()que converte para Fahrenheit (F = C * 9/5 + 32). - Exercício 3: Adicione um método estático à classe
Matematicachamadofatorial(n)que calcula o fatorial de um número inteiro não negativo. - Exercício 4: Crie uma classe
Retangulocom camposlarguraealtura(públicos) e um getterareaque retorna a área. Teste com um retângulo 5x3. - Exercício 5: Explique o que acontece quando você passa um método de uma instância como callback (ex:
setTimeout) e como corrigir usando arrow function. Escreva um exemplo.
class Livro {
constructor(titulo, autor) {
this.titulo = titulo;
this.autor = autor;
}
descricao() {
return `Título: ${this.titulo}, Autor: ${this.autor}`;
}
}
const livro = new Livro('1984', 'George Orwell');
console.log(livro.descricao()); // "Título: 1984, Autor: George Orwell"
class Temperatura {
#celsius;
setCelsius(valor) {
this.#celsius = valor;
}
getFahrenheit() {
return this.#celsius * 9/5 + 32;
}
}
const t = new Temperatura();
t.setCelsius(25);
console.log(t.getFahrenheit()); // 77
class Matematica {
static fatorial(n) {
if (n === 0 || n === 1) return 1;
return n * Matematica.fatorial(n - 1);
}
}
console.log(Matematica.fatorial(5)); // 120
class Retangulo {
constructor(largura, altura) {
this.largura = largura;
this.altura = altura;
}
get area() {
return this.largura * this.altura;
}
}
const r = new Retangulo(5, 3);
console.log(r.area); // 15
Quando você passa um método como callback, o this perde a referência à instância e se torna undefined (em strict mode) ou o objeto global. Para corrigir, podemos usar uma arrow function que captura o this do contexto léxico.
class Pessoa {
constructor(nome) {
this.nome = nome;
}
cumprimentar() {
console.log(`Olá, eu sou ${this.nome}`);
}
}
const p = new Pessoa('João');
setTimeout(p.cumprimentar, 100); // Erro: this indefinido
// Correto:
setTimeout(() => p.cumprimentar(), 100); // Funciona