|

Entendendo Classes em JavaScript

Entendendo Classes em JavaScript Foto por Ferenc Almasi em Unsplash

Introdução

JavaScript é uma linguagem baseada em protótipos, e todo objeto em JavaScript possui uma propriedade interna oculta chamada [[Prototype]] que pode ser usada para estender propriedades e métodos do objeto.

Até recentemente, desenvolvedores usavam funções construtoras para imitar um padrão de projeto orientado a objetos em JavaScript. A especificação ECMAScript 2015, frequentemente chamada de ES6, introduziu classes na linguagem JavaScript. Classes em JavaScript não oferecem funcionalidades adicionais, sendo muitas vezes descritas como fornecedoras de “açúcar sintático” para protótipos e herança, pois oferecem uma sintaxe mais limpa e elegante. Como outras linguagens de programação utilizam classes, a sintaxe de classes em JavaScript facilita que desenvolvedores realizem a transição entre linguagens.

Classes São Funções

Uma classe JavaScript é um tipo de função. Classes são declaradas com a palavra-chave class. Usaremos a sintaxe de função para inicializar uma função e a sintaxe de classe para inicializar uma classe.

// Inicializa uma função com uma expressão de função
const x = function() {}
// Inicializa uma classe com uma expressão de classe
const y = class {}

Podemos acessar o [[Prototype]] de um objeto usando o método Object.getPrototypeOf(). Agora usaremos isto para testar a função vazia que acabamos de criar.

Object.getPrototypeOf(x);
Saída
ƒ () { [native code] }

Também podemos usar esse método na classe que acabamos de criar.

Object.getPrototypeOf(y);
Saída
ƒ () { [native code] }

Ambos os segmentos declarados com function e class retornam uma função [[Prototype]]. Com protótipos, qualquer função pode se tornar uma instância de construtor usando a palavra-chave new.

const x = function() {}

// Inicializa um construtor a partir de uma função
const constructorDaFuncao = new x();

console.log(constructorDaFuncao);
Saída
x {}
constructor: ƒ ()

Isso se aplica também às classes.

const y = class {}

// Inicializa um construtor a partir de uma classe
const constructorDaClasse = new y();

console.log(constructorDaClasse);
Saída
y {}
constructor: class

Esses exemplos de construtores prototype estão vazios, mas podemos ver que por trás da sintaxe ambos os métodos estão alcançando o mesmo resultado final.

Definindo uma Classe

Uma função construtora é inicializada com vários parâmetros que seriam atribuídos como propriedades de this, referindo-se à própria função. A primeira letra do identificador seria maiúscula por convenção.

// Inicializa uma função construtora
function Drink(type, amount) {
    this.type = type;
    this.amount = amount;
}

Quando traduzimos isso para a sintaxe de classe, mostrada abaixo, vemos que ela é estruturada de maneira muito semelhante.

// Inicializa uma definição de classe
class Drink {
    constructor(type, amount) {
        this.type = type;
        this.amount = amount;
    }
}

Sabemos que uma função construtora pretende ser um modelo de objeto pela maiúscula da primeira letra do inicializador (que é opcional) e através da familiaridade com a sintaxe. A palavra-chave class comunica de maneira mais direta o objetivo da nossa função.

A única diferença na sintaxe da inicialização é o uso da palavra-chave class em vez de function, e a atribuição das propriedades dentro de um método constructor().

Definindo Métodos

A prática comum em funções construtoras é atribuir métodos diretamente ao prototype, em vez de na inicialização, como visto no método confirmDrink() abaixo.

function Drink(type, amount) {
    this.type = type;
    this.amount = amount;
}

// Adicionando um método ao construtor
Drink.prototype.confirmDrink = function() {
    return `O pedido é ${this.amount} de ${this.name}.`;
}

Com classes a sintaxe é simplificada, e o método pode ser adicionado diretamente à classe. Usando a definição de método introduzida no ES6, definir um método é um processo ainda mais conciso.

class Drink {
    constructor(type, amount) {
        this.type = type;
        this.amount = amount;
    }

    // Adiciona um método ao construtor
    confirmDrink() {
        return `O pedido é ${this.amount}ml de ${this.name}.`;
    }
}

Vamos dar uma olhada nessas propriedades e métodos em ação. Criaremos uma nova instância de Drink usando a palavra-chave new, e atribuiremos alguns valores a ela.

const drink1 = new Drink('Coke', 200);

Se imprimirmos mais informações sobre nosso novo objeto com console.log(drink1), podemos ver mais detalhes sobre o que está acontecendo com a inicialização da classe.

Saída
Drink {type: "Coke", amount: 200}
__proto__:
constructor: class Drink
confirmDrink: ƒ confirmDrink()

Podemos ver na saída que as funções constructor() e confirmDrink() foram aplicadas ao __proto__, ou [[Prototype]] de drink1, e não diretamente como um método do objeto drink1. Embora isso seja claro ao criar funções construtoras, não é óbvio ao criar classes. Classes permitem uma sintaxe mais simples e sucinta, mas sacrificam alguma clareza no processo.

Estendendo uma Classe

Uma característica vantajosa de funções construtoras e classes é que elas podem ser estendidas em novos modelos de objeto baseados na classe pai. Isso evita a repetição de código para objetos que são semelhantes, mas que precisam de recursos adicionais ou mais específicos.

Novas funções construtoras podem ser criadas a partir do pai usando o método call(). No exemplo abaixo, criaremos uma classe de bebida mais específica chamada Coffee, e atribuiremos as propriedades de Bebida a ela usando call(), além de adicionar uma propriedade adicional.

// Cria uma nova função construtora a partir do pai
function Coffee(type, amount, flavor) {
    // Use o construtor com call
    Drink.call(this, type, amount);

    this.flavor = flavor;
}

Neste ponto, podemos criar uma nova instância de Coffee usando as mesmas propriedades de Drink, bem como uma nova que adicionamos.

const drink2 = new Coffee('Espresso', 300, 'Iced Matcha');

Ao mostrar drink2 no console, podemos ver que criamos um novo Coffee com base no construtor.

Saída
Coffee {type: "Espresso", amount: 300, flavor: "Iced Matcha"}
__proto__:
constructor: ƒ Coffee(type, amount, flavor)

Com as classes ES6, a palavra-chave super é usada em vez de call para acessar as funções do pai. Usaremos extends para se referir à classe pai.

// Criando uma nova classe a partir do pai
class Coffee extends Drink {
    constructor(type, amount, flavor) {
        // Use o construtor com super
        super(type, amount);

        // Adicione uma nova propriedade
        this.flavor = flavor;
    }
}

Agora podemos criar uma nova instância de Coffee da mesma maneira.

const drink2 = new Coffee('Espresso', 300, 'Iced Matcha');

Vamos mostrar drink2 no console e ver o que aparece:

Saída
Coffee {type: "Espresso", amount: 300, flavor: "Iced Matcha"}
__proto__: Drink
constructor: class Coffee

A saída é quase exatamente a mesma, exceto que na construção da classe o [[Prototype]] está vinculado ao pai, neste caso Drink.

Abaixo está uma comparação lado a lado de todo o processo de inicialização, adição de métodos e herança de uma função construtora e uma classe.

function Drink(type, amount) {
    this.type = type;
    this.amount = amount;
}

// Adiciona um método à função construtora
Drink.prototype.confirmDrink = function() {
    return `O pedido é ${this.amount}ml de ${this.name}.`;
}

// Criando uma nova função construtora a partir do pai
function Coffee(type, amount, flavor) {
    // Use o construtor com call
    Drink.call(this, type, amount);

    this.flavor = flavor;
}
// Inicializa uma classe
class Drink {
    constructor(type, amount) {
        this.type = type;
        this.amount = amount;
    }

    // Adiciona um método à função construtora
    confirmDrink() {
        return `O pedido é ${this.amount}ml de ${this.name}.`;
    }
}

// Cria uma nova classe a partir do pai
class Coffee extends Drink {
    constructor(type, amount, flavor) {
        // Encadeie o construtor com super
        super(type, amount);

        // Adicione uma nova propriedade
        this.flavor = flavor;
    }
}

Embora a sintaxe seja bastante diferente, o resultado é fundamentalmente o mesmo com ambos os métodos. As classes nos oferecem uma maneira mais concisa de criar modelos de objetos, e as funções construtoras descrevem com mais precisão o que está acontecendo por trás dos panos.

Conclusão

Neste artigo, aprendemos sobre as semelhanças e diferenças entre funções construtoras JavaScript e classes ES6. Tanto classes quanto construtores imitam um modelo de herança orientado a objetos para JavaScript, que é uma linguagem de herança baseada em protótipos.

Compreender a herança prototípica é fundamental para ser um desenvolvedor JavaScript eficaz. Estar familiarizado com classes é extremamente útil, já que algumas bibliotecas JavaScript populares fazem uso frequente da sintaxe de classe.

Este artigo foi baseado e adaptado para Português com base no tutorial Understanding Classes in JavaScript publicado pela Digital Ocean.