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: classEsses 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 CoffeeA 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.