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.