Executando verificação de segurança...
5

[ Conteúdo ] Tudo que você precisa saber sobre Classes no JavaScript (parte 1)

Introdução:

Não é de hoje que classes é um dos tópicos mais interessantes do JavaScript. Isso porquê classes é praticamente a essência do POO. Apesar de programadores Front-end estarem diminuindo seu uso devido ao desencorajamento de alguns frameworkds como React, ainda é um assunto de extrema importância para ser aprendido. Isso eu falo para todos que aplicam código na prática.

Observações:

  • Este conteúdo busca abranger ao máximo sobre classes no javaScript. Entretanto, é impossível falar sobre todos os conceitos que fazem parte de Classes, pois é algo muito extenso para um único aritigo.

  • Tentarei ser o mais didático possível, portanto posso cobrir vários aspectos que estão fortimente relacionados com classes para um melhor entendimento. Sempre que eu for explicar mais sobre um conteúdo que esta relacionado, mas não necessariamente é tão relavante quanto o assunto principal, irei colocar um prefixo no título com [ Next ] caso queria pular pois já sabe o conteúdo.

  • Alguns textos e exemplos eu tirei direto da documentação, pois eu julguei didatico o suficiente para repassar. Alguns textos podem ser adaptados para um melhor entendimento, assim como os exemplos também.

  • Alguns tópicos vão ficar faltando propositalmente como static properties, private properties, public properties,dentre outras por questão de demanda. O artigo ficará muito longo e estou com pouco tempo para dedica-lo. Futuramente poderei criar uma parte 2 para continuar.

Exemplo prático de classes [ Next ]

Se você tá se perguntando como e onde utilizar classes, a resposta é simples. Você pode utilizar classes para tudo. É o ideal? JAMAIS! Cada problema tem suas soluções, e POO resolve muito deles, mas não todos. Então mesmo que seja possível utilizar classes para resolver um problema, se pergunte se é mesmo necessário utilizar classes ou tem uma forma melhor para isso.

Aqui está um exemplo de um simples código que calcula IMC com classes e sem classes:

class CalculadoraIMC {
    constructor(peso, altura) {
        this.peso = peso;
        this.altura = altura;
    }

    calcularIMC() {
        // Fórmula do IMC: peso / (altura * altura)
        const imc = this.peso / (this.altura * this.altura);
        return imc.toFixed(2); // Retorna o IMC com duas casas decimais
    }

    interpretarIMC() {
        const imc = this.calcularIMC();
        let interpretacao = '';

        if (imc < 18.5) {
            interpretacao = 'Abaixo do peso';
        } else if (imc < 25) {
            interpretacao = 'Peso normal';
        } else if (imc < 30) {
            interpretacao = 'Sobrepeso';
        } else {
            interpretacao = 'Obesidade';
        }

        return interpretacao;
    }
}

// Exemplo de uso da classe
const peso = 70; // em kg
const altura = 1.75; // em metros

const calculadora = new CalculadoraIMC(peso, altura);
const imc = calculadora.calcularIMC();
const interpretacao = calculadora.interpretarIMC();

console.log(`Seu IMC é: ${imc}`);
console.log(`Interpretação: ${interpretacao}`);

Sem classes:

const peso = 54.0;
const altura = 1.60;

function calculaIMC() {
    const imc = peso / (altura * altura);
    return imc.toFixed(2);
}

function interpretar() {
    const currentIMC = calculaIMC()
    let interpretacao = '';

    if (imc < 18.5) {
          interpretacao = 'Abaixo do peso';
    } else if (imc < 25) {
          interpretacao = 'Peso normal';
    } else if (imc < 30) {
          interpretacao = 'Sobrepeso';
    } else {
          interpretacao = 'Obesidade';
    }
    
    return interpretacao;
}

console.log(`seu imc é: ${calculaIMC()}`);
console.log(`seu estado é: ${interpretar()}`)

A ideia é monstrar as duas abordagens e não compara-las. Acredito que para uma aplicação de IMC, tudo bem escolher o que você achar melhor, mas para outros casos, recomendo pensar a melhor abordagem assim como tudo na programação.

Use a ferramenta certa no lugar certo.

Sumário

  • introdução:
    • Visão geral
      • Definindo classes
      • Corpo da classe
      • Constructor
      • Inicialização de blocos estáticos
    • Methods
      • Métodos estáticos e campos
      • Declaração de campos
    • Propriedades privadas
      • Herança
      • Ordem de avaliação
    • As 3 formas de definir classes
    • Exemplo de recursão com classes
      • Recursão simples
    • Tipos de classes

introdução

Nesse capitulo irei cobrir os aspectos mais básicos de classes de maneira geral. Eventualmente falei de algum assunto abordado aqui para pra frente, só que de forma mais aprofundada um pouco.

Visão geral

Classes são como "funções especiais". Tanto que podemos definir expressões de funções e declarar funções em classes (métodos). Uma classe pode ser definida de duas forma: Declarando com a palavra chave class ou declarar a classe como uma expressão.

Definindo classes

// Declaração
class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

// Expressão anônima
const Rectangle = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

// Expressão com seu próprio nome 
const Rectangle = class Rectangle2 {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

Assim como expresões funções, classes podem ser atribuidas a variáveis e ser anônimo, ou ter seu próprio nome que pode ser diferente do nome de sua variável. Entretanto existem sim algumas diferenças. A primeira é que declarações de classes está sujeito a: temporal dead zone que é uma restrição de const e let. A e outro detalhe: Expressões de classes não são içadas. O que isso quer dizer? quer dizer que você não pode chamar expressões de classes antes de declarar elas, assim como é possível em declarações de classes.

A diferença mais diferença entre essas três eu irei falar sobre eventualmente mais a frente. A ideia é apenas apresentar a posibilidade por agora.

Corpo de classe

O corpo da classes é tudo aquilo dentro das chaves {}. Dentro desta chaves você pode definir campos, métodos, controlar sua visibilidade, inicializar campos, dentre outras coisas.

Uma coisa muito interessante é que mesmo que você nâo esteja usando o strict mode, tudo dentro do escopo de classes é interpretado como se estivesse usando strict mode.

Para quem não sabe strict mode é uma deretiva que ajuda na capturação de erros

Podemos caracterizar elementos de classes de três formas:

  • Tipo: Getter, setter, method ou field
  • Localização: Static ou instância
  • Visibilidade: Public ou Private

Em visibilidade existem 2 tipos public e private, mas isso no JavaScript. Se você estiver utilizando TypeScript, existe uma outra chamada de protect

Juntando tudas essas informação, podemos chegar a conclusão que existem 16 possibilidades com para dividir lógica com classes, o que torna muito fléxivel. Irei aprofundar ainda mais sobre esse assunto mais a frente!

Constructor

O método constructor é um método especial para criar e inicializar um objeto criado com uma classe. Só pode haver apenas um método constructor por classe. Caso contrário irá lançar um Syntax Error.

Se você não conhece muito sobre erros e seus tipos, além de manipular eles, eu fiz um post que explica tudo isso aqui: tratamento de erros com javaScript

Caso a classe seja instanciada, é necessário chamar o método super().

Exemplo:

class Rectangle {
  height;
  width;
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

Caso sua classe não precise de um construtor, ele será por padrão vazio, ou seja: undefined

Inicializando blocos estáticos

Essa é uma feature mais recente do javaScript que vou explicar de forma mais aprofundada mais pra frente. A ideia aqui é apenas apresentar a feature.

Essa nova feature trás mais flexibilidade na inicialização de propriedades estática (static properties), incluindo a leitura de declaração durante a incialização. Além disso, garante acesso a escopo privado.

Posso declarar múlplos blocos estáticos e podem ser intercalados com declarações de campos estáticos e métodos.

Todos os elementos estáticos são avaliados na ordem de declaração.

Methods

Métodos são definidos no protótipo de cada instância de uma classes e compartilha entre instâncias. Métodos pode ser simples funções, funções assíncronas, functions generators ou assíncrono generator function.

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // Getter
  get area() {
    return this.calcArea();
  }
  // Method
  calcArea() {
    return this.height * this.width;
  }
  *getSides() {
    yield this.height;
    yield this.width;
    yield this.height;
    yield this.width;
  }
}

const square = new Rectangle(10, 10);

console.log(square.area); // 100
console.log([...square.getSides()]); // [10, 10, 10, 10]

Métodos estáticos

A palavra chave static define um método estático ou um campo estático para a classe. Propriedade estáticas (campos ou métodos) são definidas na classe em si ao invés de cada instância. Métodos estáticos são muito utilizados para criar funções utilitárias enquanto os campos são para armazenamento. Também é útil para configuração fixa, ou qualquer outro tipo de dado que não precisa ser replicado entre instâncias.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static displayName = "Point";
  static distance(a, b) {
    const dx = a.x - b.x;
    const dy = a.y - b.y;

    return Math.hypot(dx, dy);
  }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);
p1.displayName; // undefined
p1.distance; // undefined
p2.displayName; // undefined
p2.distance; // undefined

console.log(Point.displayName); // "Point"
console.log(Point.distance(p1, p2)); // 7.0710678118654755

Declaração de campos

Assim como vimos anteriormente em constructor, a sintax é mais ou menos essa ao declarar um campo:

class Rectangle {
  height = 0;
  width;
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

Campos de classes são semelhantes a propriedade de objetos simples. Não são variavéis, portanto é estreitamente proíbido usar palavras chaves como const e let. No javaScript possui identificadores para sinalizar se um campo é privado ou não, mas não usando as palavras chaves como public e private como é no TypeScript por exemplo. JavScript utiliza outra syntax para isso. Portanto, não se pode usar public e private em campos.

Propriedades privadas

Campos são por padrão público ao declarar. Se quiser modifica-los para privados, utilize um hashtag para isso como abaixo.

class Rectangle {
  #height = 0;
  #width;
  constructor(height, width) {
    this.#height = height;
    this.#width = width;
  }
}

vai lançar um erro ao tentar referenciar campos privados fora da classe. Eles podem apenas ser lido ou escrito dentro do corpo da classe. Ao definir campos como privados você os torna mais seguro, previnindo que seja acessado fora de seu escopo e seja acidentalmente alterado.

Os campos privados só podem ser declarados antecipadamente numa declaração de campo. Não podem ser criados mais tarde através de atribuições, como acontece com as propriedades normais.

Herança

A palavra chave extends ainda vai aparecer muito neste artigo. extends serve para estender um classes para outra, assim permitindo uma sublclass ter suas propriedades.

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); 
    // Necessário o uso de uma chamada super() para o uso de propriedade da classe pai. 
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

const d = new Dog("BoB");
d.speak(); // BoB barks.

Se houver um construtor presente na subclasse, ele precisa primeiro chamar super() antes de usar this. A palavra-chave super também pode ser utilizada para chamar métodos correspondentes da superclasse.

class Cat {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(`${this.name} roars.`);
  }
}

const l = new Lion("Fuzzy");
l.speak();
// Fuzzy makes a noise.
// Fuzzy roars.

Ordem de avaliação

Quando nosso programa é iniaciado e nossa classe é avaliado ocorre vários processos internamente. Observe abaixo:

  1. Primeiramenta a clausúla extends é avaliada. Caso esteja usando o extends com um construtor adquado como uma function coonstructor ou null, um typeError é lançado.

  2. O método constructor é extraído, e substituido com um valor padrão se não estiver presente. Devido a natureza da operação, esta etapa não é observável

  3. As chaves de propriedade dos elementos de classe são avaliadas na ordem de declaração. Se a chave da propriedade for computada, a expressão computada é avaliada, com o valor this definido como o valor this ao redor da classe (não a própria classe). Nenhum dos valores de propriedade é avaliado ainda.

  4. Métodos e acessores são instalados na ordem de declaração. Métodos de instância e acessores são instalados na propriedade prototype da classe atual, e métodos estáticos e acessores são instalados na própria classe. Métodos de instância privados e acessores são salvos para serem instalados na instância diretamente mais tarde. Este passo não é observável.

  5. A classe é agora inicializada com o protótipo especificado por extends e a implementação especificada por constructor. Para todos os passos acima, se uma expressão avaliada tentar acessar o nome da classe, um ReferenceError é lançado porque a classe ainda não foi inicializada.

  6. Os valores dos elementos de classe são avaliados pela ordem da declaração:

    • Para cada campo de instância (público ou privado), sua expressão inicializadora é salva. O inicializador é avaliado durante a criação da instância, no início do construtor (para classes base) ou imediatamente antes da chamada super() retornar (para classes derivadas).
    • Para cada campo estático (público ou privado), seu inicializador é avaliado com this definido para a própria classe, e a propriedade é criada na classe.
    • Blocos de inicialização estáticos são avaliados com this definido para a própria classe.
  7. A classe está agora totalmente inicializada e pode ser usada como uma função de construtor.

Diferença entre as 3 definições de classes (se aprofundando):

Declaração de classe: Esta é a forma mais direta de definição de classe. Ela utiliza a palavra-chave class seguida do nome da classe. Esta forma é hoisted, o que significa que a classe pode ser usada antes de ser declarada no código. Esta é a forma mais comum e amplamente utilizada para definir classes em JavaScript.

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

Expressão de classe (anónima): Esta forma atribui uma classe a uma variável sem dar um nome à classe. A classe é anónima e só é acessível através da variável à qual é atribuída. Esta forma não é içada, o que significa que tem de ser definida antes de ser utilizada.

const Rectangle = class {
  width: number;
  height: number;  
  constructor(height:number, width:number) {
    this.height = height;
    this.width = width;
  }
};

const intance = new Rectangle(10,20);
console.log(intance.height)

Expressão de classe (nomeada): Semelhante à expressão de classe anónima, esta forma também atribui uma classe a uma variável. No entanto, a classe recebe seu próprio nome, que pode ser usado para recursão ou referência à classe dentro de seus próprios métodos. Esta forma também não é içada.

const Rectangle = class Rectangle2 {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

Melhor forma: A melhor forma a utilizar depende das suas necessidades específicas:

  • Usar Declaração de Classe quando quiser que a classe seja içada e acessível em todo o âmbito do seu código. Este é o caso de uso mais comum e é geralmente recomendado para facilitar a leitura e a manutenção.

  • Utilizar a Expressão de Classe (Anónima)** quando necessitar de uma classe acessível apenas dentro de um âmbito específico ou quando pretender evitar o hoisting. Isto é útil para criar classes privadas ou quando é necessário criar uma classe condicionalmente.

  • Use a Expressão de Classe (Nomeada)** quando precisar de referenciar a classe dentro dos seus próprios métodos ou para recursão. Esta forma também pode ser útil para criar classes privadas ou quando é necessário evitar o hoisting.

Em resumo, a forma de declaração de classe é a mais versátil e amplamente utilizada, tornando-a a melhor escolha para a maioria dos cenários. No entanto, as expressões de classe oferecem flexibilidade para casos de uso específicos, como a criação de classes privadas ou evitar o içamento. Portanto, observe bem qual é a sua necessidade e escolha com calma.

Tipos de classes (se aprofundando)

Irei descrever várias combinações de elementos de classe em JavaScript, que são caracterizados pelo seu tipo, localização e visibilidade. Estas características permitem aos programadores definir e controlar o comportamento e a acessibilidade dos membros da classe de uma forma flexível e estruturada. Vamos analisar cada aspeto:

  • Setter: Um método que define o valor de uma propriedade específica. É utilizado para definir um acessor para uma propriedade.

  • Método: Uma função que é uma propriedade de um objeto.

  • Campo: Uma variável que é uma propriedade de um objeto.

  • Localização: Especifica se o elemento de classe é estático ou de instância.

  • Estático: Elementos que pertencem à própria classe, em vez de instâncias da classe. São acedidos utilizando o nome da classe.

  • Instância: Elementos que pertencem a instâncias da classe. São acedidos através de instâncias da classe.

  • Visibilidade: Define a acessibilidade de um elemento da classe.

  • Público: Elementos que podem ser acedidos a partir de qualquer lugar, incluindo de fora da classe.

  • Privado: Elementos que só podem ser acedidos dentro da classe.

Dadas estas características, existem 16 combinações possíveis de elementos de classe em JavaScript:

  • Public Getter
  • Public Setter
  • Public Method
  • Public Field
  • Private Getter
  • Private Setter
  • Private Method
  • Private Field
  • Static Public Getter
  • Static Public Setter
  • Static Public Method
  • Static Public Field
  • Static Private Getter
  • Static Private Setter
  • Static Private Method
  • Static Private Field

Estas combinações permitem uma vasta gama de padrões de conceção de classes, desde o encapsulamento de dados e métodos em instâncias até ao fornecimento de métodos utilitários que podem ser acedidos estaticamente.


É isso pessoal, espero que tenham gostado!

illustration

Carregando publicação patrocinada...
1