Tutorial

Entendendo classes no JavaScript

DevelopmentJavaScript

Introdução

O JavaScript é uma linguagem baseada em protótipo, e cada objeto no JavaScript tem uma propriedade interna escondida chamada [[Prototype]], que pode ser usada para estender as propriedades e métodos de objetos. Você pode ler mais sobre protótipos no nosso tutorial Entendendo protótipos e herança no JavaScript.

Até recentemente, os desenvolvedores criativos usavam funções de construção para imitar um padrão de design orientado a objeto no JavaScript. A especificação de linguagem ECMAScript 2015, frequentemente chamada de ES6, introduziu classes na linguagem JavaScript. As classes no JavaScript não oferecem, de fato, funcionalidades adicionais e são muitas vezes descritas como provedoras de “açúcar sintático” em relação a protótipos e herança, sendo que estes oferecem uma sintaxe mais limpa e mais elegante. Uma vez que outras linguagens de programação usam classes, a sintaxe de classe no JavaScript torna a coisa mais simples para que desenvolvedores consigam transitar entre linguagens.

Classes são funções

Uma classe do JavaScript é um tipo de função. As classes são declaradas com a palavra-chave class. Vamos usar a sintaxe de expressão de função para inicializar uma função e a sintaxe de expressão de classe para inicializar uma classe.

// Initializing a function with a function expression
const x = function() {}
// Initializing a class with a class expression
const y = class {}

Podemos acessar o [[Prototype]] de um objeto usando o método Object.getPrototypeOf(). Vamos usar isso para testar a função vazia que criamos.

Object.getPrototypeOf(x);
Output
ƒ () { [native code] }

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

Object.getPrototypeOf(y);
Output
ƒ () { [native code] }

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

const x = function() {}

// Initialize a constructor from a function
const constructorFromFunction = new x();

console.log(constructorFromFunction);
Output
x {} constructor: ƒ ()

Isso também se aplica às classes.

const y = class {}

// Initialize a constructor from a class
const constructorFromClass = new y();

console.log(constructorFromClass);
Output
y {} constructor: class

Estes exemplos de construtores de protótipo estão aparentemente vazios, mas podemos ver que sob a sintaxe, ambos os métodos estão alcançando o mesmo resultado final.

Definindo uma classe

No tutorial de protótipos e herança, criamos um exemplo baseado na criação de personagens em um jogo RPG baseado em texto. Vamos continuar com esse exemplo para atualizar a sintaxe de funções para classes.

Uma função de construção é inicializada com um número de parâmetros que seriam atribuídos como propriedades de this, referindo-se à função em si. A primeira letra do identificador seria maiúscula por convenção.

constructor.js
// Initializing a constructor function
function Hero(name, level) {
    this.name = name;
    this.level = level;
}

Quando traduzimos isso para a sintaxe classe mostrada abaixo, vemos que ela está estruturada de maneira similar.

class.js
// Initializing a class definition
class Hero {
    constructor(name, level) {
        this.name = name;
        this.level = level;
    }
}

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

A única diferença na sintaxe de inicialização é usar a palavra-chave class ao invés de function, e atribuir as propriedades dentro de um método constructor().

Definindo métodos

Uma prática comum com funções de construção é atribuir métodos diretamente ao prototype ao invés da inicialização, como visto no método greet() abaixo.

constructor.js
function Hero(name, level) {
    this.name = name;
    this.level = level;
}

// Adding a method to the constructor
Hero.prototype.greet = function() {
    return `${this.name} says hello.`;
}

Com classes, esta sintaxe é simplificada e o método pode ser adicionado diretamente à classe. Ao usar a forma abreviada da definição do método introduzida como sendo ES6, definir um método é um processo ainda mais conciso.

class.js
class Hero {
    constructor(name, level) {
        this.name = name;
        this.level = level;
    }

    // Adding a method to the constructor
    greet() {
        return `${this.name} says hello.`;
    }
}

Vamos ver essas propriedades e métodos em ação. Criaremos uma nova instância Hero usando a palavra-chave new e atribuiremos alguns valores.

const hero1 = new Hero('Varg', 1);

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

Output
Hero {name: "Varg", level: 1} __proto__: ▶ constructor: class Hero ▶ greet: ƒ greet()

Podemos ver no resultado que as funções constructor() e greet() foram aplicadas ao __proto__, ou [[Prototype]] do hero1, e não diretamente como um método no objeto hero1. Embora isso seja claro ao criar funções de construção, não é óbvio ao criar classes. As classes permitem uma sintaxe mais simples e sucinta, mas sacrifica um pouco de clareza no processo.

Estendendo uma classe

Uma característica vantajosa de funções de construção e classes é que elas podem ser estendidas para novos projetos de objeto baseados no pai. Isso impede a repetição de código para objetos semelhantes, mas precisa de algumas características adicionais ou mais específicas.

Novas funções de construção podem ser criadas a partir do pai usando o método call(). No exemplo abaixo, vamos criar uma classe de personagens mais específica chamada Mage e atribuir as propriedades de Hero a ela usando o call(), assim como adicionar uma propriedade adicional.

constructor.js
// Creating a new constructor from the parent
function Mage(name, level, spell) {
    // Chain constructor with call
    Hero.call(this, name, level);

    this.spell = spell;
}

Neste ponto, podemos criar uma nova instância de Mage usando as mesmas propriedades que o Hero assim como uma nova que adicionamos.

const hero2 = new Mage('Lejon', 2, 'Magic Missile');

Ao enviar hero2 para o console, podemos ver que criamos um novo Mage baseado no construtor.

Output
Mage {name: "Lejon", level: 2, spell: "Magic Missile"} __proto__: ▶ constructor: ƒ Mage(name, level, spell)

Com classes ES6, a palavra-chave super é usada no lugar de call para acessar as funções do pai. Vamos usar extends para nos referir à classe pai.

class.js
// Creating a new class from the parent
class Mage extends Hero {
    constructor(name, level, spell) {
        // Chain constructor with super
        super(name, level);

        // Add a new property
        this.spell = spell;
    }
}

Agora, podemos criar uma nova instância Mage da mesma maneira.

const hero2 = new Mage('Lejon', 2, 'Magic Missile');

Vamos imprimir hero2 para o console e visualizar o resultado.

Output
Mage {name: "Lejon", level: 2, spell: "Magic Missile"} __proto__: Hero ▶ constructor: class Mage

O resultado é quase exatamente o mesmo, exceto que na construção de classes o [[Prototype]] está ligado ao pai, neste caso, Hero.

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

constructor.js
function Hero(name, level) {
    this.name = name;
    this.level = level;
}

// Adding a method to the constructor
Hero.prototype.greet = function() {
    return `${this.name} says hello.`;
}

// Creating a new constructor from the parent
function Mage(name, level, spell) {
    // Chain constructor with call
    Hero.call(this, name, level);

    this.spell = spell;
}
class.js
// Initializing a class
class Hero {
    constructor(name, level) {
        this.name = name;
        this.level = level;
    }

    // Adding a method to the constructor
    greet() {
        return `${this.name} says hello.`;
    }
}

// Creating a new class from the parent
class Mage extends Hero {
    constructor(name, level, spell) {
        // Chain constructor with super
        super(name, level);

        // Add a new property
        this.spell = spell;
    }
}

Embora a sintaxe seja bastante diferente, o resultado fundamental é quase idêntico entre ambos os métodos. As classes dão-nos uma maneira mais concisa de criar plantas de objetos e as funções de construção descrevem com maior precisão o que está acontecendo nas entrelinhas.

Conclusão

Neste tutorial, aprendemos sobre as semelhanças e diferenças entre funções de construção e classes ES6 do JavaScript. Ambas classes e funções de construção imitam um modelo de herança orientado a objeto para JavaScript, que é uma linguagem de herança baseada em protótipo.

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

0 Comments

Creative Commons License