Classes ES6: sintaxe e herança

1. Introdução às Classes ES6

Antes do ES6, JavaScript utilizava funções construtoras e protótipos para simular orientação a objetos. Com a especificação ECMAScript 2015 (ES6), as classes foram introduzidas como syntactic sugar sobre o sistema de protótipos existente. Isso significa que, por baixo dos panos, continuamos trabalhando com protótipos, mas a sintaxe se tornou mais limpa e familiar para desenvolvedores de linguagens como Java ou C++.

// Função construtora tradicional (pré-ES6)
function Pessoa(nome) {
    this.nome = nome;
}
Pessoa.prototype.saudacao = function() {
    return `Olá, eu sou ${this.nome}`;
};

// Classe ES6 equivalente
class Pessoa {
    constructor(nome) {
        this.nome = nome;
    }
    saudacao() {
        return `Olá, eu sou ${this.nome}`;
    }
}

Um ponto crucial: classes em JavaScript operam em strict mode automaticamente, mesmo sem a diretiva "use strict". Além disso, diferentemente de funções construtoras, classes não sofrem hoisting — você não pode usar uma classe antes de declará-la.

2. Sintaxe de Classes: Métodos e Propriedades

As classes ES6 permitem definir métodos de instância e métodos estáticos de forma clara. Métodos estáticos são chamados diretamente na classe, não em instâncias.

class Matematica {
    static somar(a, b) {
        return a + b;
    }

    static PI = 3.14159; // Campo estático (ES2022)

    constructor(valor) {
        this.valor = valor;
    }

    // Getter
    get valorDobrado() {
        return this.valor * 2;
    }

    // Setter
    set valorDobrado(novoValor) {
        this.valor = novoValor / 2;
    }
}

console.log(Matematica.somar(2, 3)); // 5
const m = new Matematica(10);
console.log(m.valorDobrado); // 20

Desde o ES2022, podemos declarar campos públicos diretamente no corpo da classe, sem necessidade do construtor:

class Usuario {
    nome = 'Anônimo'; // Campo público
    #senha = '';      // Campo privado (veremos adiante)

    constructor(nome, senha) {
        this.nome = nome || this.nome;
        this.#senha = senha;
    }
}

3. O Construtor e a Criação de Instâncias

O método constructor é especial — ele é executado automaticamente quando usamos new. É o local ideal para inicializar propriedades da instância.

class Animal {
    constructor(nome, especie) {
        this.nome = nome;
        this.especie = especie;
        this.vivo = true;
    }
}

const gato = new Animal('Mimi', 'felino');

Em subclasses, o construtor deve chamar super() antes de usar this. Se você não definir um construtor na subclasse, o JavaScript cria um automaticamente que chama super(...args).

class Mamifero extends Animal {
    constructor(nome, especie, temPelos) {
        super(nome, especie); // Obrigatório!
        this.temPelos = temPelos;
    }
}

Um detalhe importante: se o construtor retornar explicitamente um objeto, esse objeto será usado como resultado de new, ignorando a instância criada automaticamente. Isso é raro, mas possível.

4. Herança com extends

A palavra-chave extends estabelece uma cadeia de protótipos entre classes. A classe filha herda todos os métodos e propriedades da classe pai.

class Veiculo {
    constructor(marca, ano) {
        this.marca = marca;
        this.ano = ano;
    }

    descricao() {
        return `${this.marca} (${this.ano})`;
    }
}

class Carro extends Veiculo {
    constructor(marca, ano, portas) {
        super(marca, ano);
        this.portas = portas;
    }

    // Override do método
    descricao() {
        return `${super.descricao()} - ${this.portas} portas`;
    }

    buzinar() {
        return 'Biiii!';
    }
}

const fusca = new Carro('Volkswagen', 1970, 2);
console.log(fusca.descricao()); // Volkswagen (1970) - 2 portas
console.log(fusca.buzinar());   // Biiii!

Note o uso de super.descricao() para acessar o método da classe pai dentro do método sobrescrito.

5. Mixins e Herança Múltipla Simulada

JavaScript não suporta herança múltipla diretamente. Uma classe só pode estender uma única classe base. No entanto, podemos simular esse comportamento com mixins — funções que combinam comportamentos de múltiplas fontes.

// Mixins como funções auxiliares
const Navegavel = {
    navegar() {
        return 'Navegando...';
    }
};

const Flutuavel = {
    flutuar() {
        return 'Flutuando...';
    }
};

// Função que aplica mixins a uma classe
function aplicarMixins(targetClass, ...mixins) {
    Object.assign(targetClass.prototype, ...mixins);
}

class Barco {
    constructor(nome) {
        this.nome = nome;
    }
}

aplicarMixins(Barco, Navegavel, Flutuavel);
const barco = new Barco('Titanic');
console.log(barco.navegar()); // Navegando...
console.log(barco.flutuar()); // Flutuando...

Em React, antes dos hooks, era comum usar mixins com React.createClass. Hoje, com componentes de classe, prefere-se composição ou Higher-Order Components (HOCs).

6. Campos Privados e Encapsulamento

O ES2022 introduziu campos privados reais usando o prefixo #. Diferente da convenção _ (que era apenas uma sugestão), campos com # são verdadeiramente privados — inacessíveis fora da classe.

class ContaBancaria {
    #saldo = 0; // Campo privado

    constructor(titular, saldoInicial) {
        this.titular = titular;
        this.#saldo = saldoInicial;
    }

    #validarSaque(valor) { // Método privado
        return valor <= this.#saldo;
    }

    depositar(valor) {
        if (valor > 0) {
            this.#saldo += valor;
        }
    }

    sacar(valor) {
        if (this.#validarSaque(valor)) {
            this.#saldo -= valor;
            return true;
        }
        return false;
    }

    get saldo() {
        return this.#saldo;
    }
}

const conta = new ContaBancaria('João', 1000);
console.log(conta.saldo); // 1000 (via getter)
// console.log(conta.#saldo); // Erro! Propriedade privada

A convenção _ (ex.: _saldo) ainda é amplamente usada, mas não oferece proteção real — apenas sinaliza que a propriedade não deveria ser acessada externamente.

7. Classes no Ecossistema Node.js e React

Em Node.js, classes são comumente exportadas como módulos:

// utils/Validador.js
export class Validador {
    static email(email) {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
    }

    static cpf(cpf) {
        // Lógica de validação...
        return true;
    }
}

// app.js
import { Validador } from './utils/Validador.js';
console.log(Validador.email('teste@exemplo.com')); // true

Em React, componentes de classe ainda são suportados e utilizam métodos de ciclo de vida:

import React, { Component } from 'react';

class Contador extends Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
    }

    componentDidMount() {
        console.log('Componente montado!');
    }

    shouldComponentUpdate(nextProps, nextState) {
        return nextState.count !== this.state.count;
    }

    render() {
        return (
            <div>
                <p>Contagem: {this.state.count}</p>
                <button onClick={() => this.setState({ count: this.state.count + 1 })}>
                    Incrementar
                </button>
            </div>
        );
    }
}

Apesar dos hooks terem se tornado o padrão no React moderno, componentes de classe ainda são amplamente encontrados em bases de código legadas e em situações que exigem métodos de ciclo de vida específicos.

8. Boas Práticas e Armadilhas Comuns

Não esquecer o new: Chamar uma classe sem new lança um TypeError.

const p = Pessoa('João'); // TypeError: Class constructor Pessoa cannot be invoked without 'new'

Hoisting: Ao contrário de funções, classes não são içadas:

// Isto funciona:
const a = soma(2, 3);
function soma(a, b) { return a + b; }

// Isto NÃO funciona:
const obj = new MinhaClasse(); // ReferenceError
class MinhaClasse {}

Perda de contexto this: Em callbacks, o this pode se perder. Use arrow functions ou .bind():

class Botao {
    constructor(texto) {
        this.texto = texto;
    }

    clicar() {
        console.log(`Clicou em: ${this.texto}`);
    }

    // Solução 1: arrow function
    clicarArrow = () => {
        console.log(`Clicou em: ${this.texto}`);
    }
}

const btn = new Botao('Enviar');
document.getElementById('meuBotao').addEventListener('click', btn.clicar); // this = undefined
document.getElementById('meuBotao').addEventListener('click', btn.clicarArrow); // Correto

Preferência por classes vs funções em React: Embora componentes de classe ainda funcionem, o ecossistema React moderno favorece componentes funcionais com hooks. Use classes apenas quando houver necessidade explícita de métodos de ciclo de vida ou quando mantendo código legado.


Referências