Entendendo Classes e Prototype no Javascript
Neste artigo tentarei abordar de forma simples e objetiva o que eu aprendi (obrigado, Will Sentance) sobre a orientação a objetos no Javascript, que é um paradigma de programação popular para estruturar códigos complexos.
Partirei então com o princípio da encapsulação, em como podemos armazenar dados e suas respectivas funções associadas a esses dados: Objetos.
Comecemos então com o objeto veículo:
const vehicle1 = {
type: "Car",
name: "Belina",
speed: 0,
accelerate: function() { vehicle.speed++; }
};
Precisamos agora criar um novo veículo, continuemos então a criar, mas de maneiras diferentes.
1. Utilizando dot notation 🔨
const vehicle2 = {}; // cria um objeto vazio.
// atribui as propriedades p/ o objeto.
vehicle.type = "Car";
vehicle.name = "Monza";
vehicle.speed = 0;
vehicle.accelerate = function() {
vehicle2.speed++;
}
2. Utilizando Object.create() 🛠️
const vehicle3 = Object.create(null);
// atribui as propriedades p/ o objeto.
vehicle3.type = "Car";
vehicle3.name = "Uno";
vehicle3.speed = 0;
vehicle3.accelerate = function() {
vehicle3.speed++;
}
Ok, todas as formas acima são válidas para criarmos um objeto, porém você deve estar se sentindo desconfortável com a maneira que estamos fazendo isso. Para cada veículo novo que formos criar teremos que copiar e repetir várias linhas de código.
Estamos ferindo um princípio muito importante da programação aqui, DRY (Don’t Repeat Yourself), ou seja, estamos repetindo código desnecessariamente.
O que podemos fazer?
Factories! Gerando objetos utilizando funções 🏭
function vehicleCreator(type, name, speed) {
const newVehicle = {};
newVehicle.type = type;
newVehicle.name = name;
newVehicle.speed = 0;
newVehicle.accelerate = function () {
newVehicle.speed++;
};
return newVehicle;
};
Agora conseguimos eliminar a cópia de código sempre que precisamos criar um novo veículo. Ficou muito mais simples e fácil de realizar essa tarefa.
const vehicle1 = vehicleCreator("Car", "Belina", 0);
const vehicle2 = vehicleCreator("Car", "Monza", 0);
vehicle1.accelerate();
Resolvemos alguns de nossos problemas.
Em contrapartida, cada vez que utilizamos essa abordagem nós estamos criando cópias na memória da nossa aplicação para cada novo carro criado.
Isso não é um problema em relação as propriedades, pois cada veículo tem a sua, porém estamos desnecessariamente criando cópias do método accelerate, uma vez que ele tem o mesmo comportamento em todos os nossos veículos. That’s sucks!
Utilizando Prototype chain ⛓️
E se ao invés de criarmos uma cópia de cada método, a cada novo veículo, armazenássemos os nossos métodos em algum outro local e vinculássemos esse métodos a cada objeto criado, de uma maneira que se o interpretador não encontrar o método accelerate no objeto vehicle1, ele irá checar na cadeia de protótipo se o método existe, e encontrará!
Podemos criar esse vínculo utilizando o método Object.create();
function vehicleFunctionStore() {
accelerate: function() { this.speed++; }
}
function vehicleCreator(type, name, speed) {
// o Object.create() serve para fazer o vínculo entre o novo objeto criado e os métodos do objeto vehicleFunctionStore.
const newVehicle = Object.create(vehicleFunctionStore);
newVehicle.type = type;
newVehicle.name = name;
newVehicle.speed = 0;
return newVehicle;
}
Agora a maneira que criamos e utilizamos o método de cada objeto continua o mesmo, porém não estamos mais criando cópias desnecessárias do método na memória.
const vehicle1 = vehicleCreator("Car", "Belina", 0);
const vehicle2 = vehicleCreator("Car", "Monza", 0);
vehicle1.accelerate();
Mas, como funciona esse vínculo entre vehicleFunctionStore e vehicleCreator que acabamos de criar?
Todos os objetos no Javascript possuem uma propriedade chamada proto, que serve para linkar todos os objetos criados a um outro grande objeto, Object.prototype, que contém diversos outros métodos que podemos utilizar.
É exatamente através dessa propriedade que teremos acesso ao nosso vehicleFunctionStore.
Quando executamos o accelerate, nosso interpretador vai até o objeto vehicle1 procurar pelo método.
Entretanto, esse método não existe no objeto, então o interpretador vai até a o Object.Prototype verificar se esse método existe, e lá consegue encontrá-lo e executá-lo!
É importante lembrar que nosso método accelerate agora utiliza o this.speed++, ou seja, quando executamos o método e o interpretador o encontra no Object.prototype, o this dependerá do contexto em que ele está sendo chamado, que no nosso caso acima será vehicle1.
Ótimo, resolvemos mais um problema! Mas as coisas ainda estão um pouco fora do padrão aqui.
Essa abordagem é útil e funciona, mas existem meios mais fáceis de fazer o trabalho duro. E podemos utilizar um atalho de apenas 3 letras:
new keyword 🆕
// Vamos mudar dessa maneira
const vehicle1 = vehicleCreator("Car", "Belina", 0);
const vehicle2 = vehicleCreator("Car", "Monza", 0);
// Para essa
const vehicle1 = new vehicleCreator("Car", "Belina", 0);
const vehicle2 = new vehicleCreator("Car", "Monza", 0);
Porém precisaremos fazer algumas adaptações em nosso código.
function vehicleCreator(type, name, speed) {
this.type = type;
this.name = name;
this.speed = speed;
}
Note que agora não precisamos mais criar um novo objeto e nem retorná-lo, a palavrinha new será a responsável por fazer esse trabalho para a gente, e é apenas isso que ele faz. Sem segredos.
A nossa função criadora agora ficou muito mais simples, mas agora ainda falta criarmos o link do nosso método com o nosso objeto, através da propriedade proto.
proto é a propriedade que todos os objetos possuem, que faz o link entre o objeto e o Object.prototype, que é um grande objeto com vários métodos.
vehicleCreator.prototype.accelerate = function() {
this.speed++;
}
Esse trecho de código tem exatamente a mesma função do que estávamos fazendo com o Object.create(vehicleFunctionStore).
Agora podemos criar os nossos objetos dessa maneira
const vehicle1 = new vehicleCreator("Car", "Belina", 0);
const vehicle2 = new vehicleCreator("Car", "Monza", 0);
Ótimo, já facilitamos muito mais a escrita, perfomance e entendimento do nosso código, mas imagine que a cada novo método teremos que criar um novo vínculo na cadeia de prototype com o nosso objeto.
vehicleCreator.prototype.accelerate = function() {
this.speed++;
}
vehicleCreator.prototype.decelerate = function() {
this.speed--;
}
Nós temos que escrever os nossos métodos separados do nosso construtor de objetos. Isso não me parece muito usual.
Classes 🎩
A partir do ES2015 tivemos a inserção da syntactic sugar class.
Isso emergiu como um novo padrão de desenvolvimento, e se parece muito com outras linguagens de programação.
Entretanto, a palavra class do Javascript, por baixo dos panos, faz exatamente a mesma coisa que fizemos na solução 3.
Ela serve apenas para tornar o nosso código mais legível, com ela, ao invés de declararmos nossos métodos separados de nosso construtores, poderemos colocá-los todos em um lugar só.
Vejamos como ficaria nossa função construtora agora:
class VehicleCreator {
constructor(type, name, speed) {
this.type = type;
this.name = name;
this.speed = speed;
}
accelerate() {
this.speed++;
}
decelerate() {
this.speed--;
}
}
const vehicle1 = new vehicleCreator("Car", "Belina", 0);
vehicle1.accelerate();
Esse trecho de código, under the hood, equivale exatamente ao código abaixo, porém é muito mais elegante.
function vehicleCreator(type, name, speed) {
this.type = type;
this.name = name;
this.speed = speed;
}
vehicleCreator.prototype.accelerate = function() {
this.speed++;
}
vehicleCreator.prototype.decelerate = function() {
this.speed--;
}
const vehicle1 = new vehicleCreator("Car", "Belina", 0);
vehicle1.accelerate();
Então é isso, pessoal.
Tentei — e espero que tenha conseguido — explicar de maneira sucinta e direta sobre prototype chain, diferença entre proto e prototype, e a maneira que as palavras new e class nos auxiliam para a criação objetos (além de como funcionam under the hood).
Até a próxima. 😃 👋
Veja também:
Entendendo closures no Javascript
Entendendo Higher Order Functions no Javascript
Meu website pessoal