Reaproveitando código com JavaScript: herança e protótipos
8 min8 minutos de leitura
8 min8 minutos de leitura
Introdução
Quando estamos desenvolvendo um sistema orientado a objetos, é comum termos uma certa preocupação com repetição de código.
Autor(a)
leonardo.wolter
Inscreva-se em nossa Newsletter
Fique por dentro de conteúdos, insights e oportunidades do universo tech. Receba novidades e lançamentos direto no seu e-mail.
Imagine que nós implementamos um sistema contendo uma entidade Pessoa (com nome e email), mas surgiu agora a necessidade temos um tipo mais específico de Pessoa: PessoaFisica, que contém todos os comportamentos de Pessoa e, além disso, sabe dizer seu CPF.
No mundo da orientação a objetos, seria comum utilizarmos herança para não precisar copiar os atributos e métodos da classe Pessoa. Mas e no JavaScript, como poderíamos implementar isso?
Existem diversos modos de se implementar herança em JavaScript. Nesse post, eu tentarei cobrir as seguintes maneiras:
Prototype-chaining Inheritance
Parasitic Combination Inheritance
Functional Inheritance
1 - Prototype-chaining Inheritance
Antes de pensarmos na herança, vamos escrever uma função construtora utilizando o Pseudo-classical pattern para representar a Pessoa :
varPessoa = function(nome, email){ this.nome = nome; this.email = email };
Pessoa.prototype.fala = function(){ console.log("Olá, meu nome é "+this.nome+" e meu email é "+this.email); };
Mas note que, deste modo, nada impede que um desenvolvedor instancie uma pessoa com um email inválido:
var leoInvalido = newPessoa("Leonardo", "emailinvalido");
Por estarmos utilizando uma função construtora, fica fácil resolver esse problema:
Bacana, já temos a nossa Pseudo-classe representando uma Pessoa. Mas, como disse anteriormente, nós precisamos de um outro tipo de pessoa: uma PessoaFisica, que possui CPF e sabe dizê-lo:
Mas note que, do jeito que implementamos a função construtora PessoaFisica, ela não está verificando se o email foi preenchido corretamente como estávamos fazendo na Pessoa!
Vamos então reaproveitar a verificação que escrevemos anteriormente. Para isso, basta chamar a função Pessoa utilizando a nossa instância (this) como referência.
Isso fará com que nossa PessoaFisica tenha todos os atributos que uma Pessoa teria!
Neste outro post, mostrei que os prototypes servem justamente para adicionar métodos a todas as instâncias de determinada função. Note que é exatamente esse o comportamento que falta em nossa classe PessoaFisica! Queremos que ela tenha todos os métodos que uma instância da função Pessoa teria.
Então, para herdarmos os métodos de Pessoa, basta setarmos o prototype da PessoaFisica para uma instância de Pessoa:
PessoaFisica.prototype = newPessoa(); PessoaFisica.prototype.constructor = PessoaFisica; // corrige o ponteiro do construtor
Obs: Alguns diriam que é uma má pratica instanciar uma Pessoa sem argumentos para atribuir ao prototype de PessoaFisica, e eles estão certos! Deste modo, além de criarmos uma Pessoa inconsistente (com os atributos nome e email vazios), acabamos por instanciar um objeto somente para atribuir ao protótipo da PessoaFisica, consumindo tempo de execução e memória. Na estratégia Parasitic Combination Inheritance veremos um jeito melhor de fazer isso!
Note que, a partir do momento que você sobrescreveu o prototype de PessoaFisica, você perdeu o método dizCpf!
Para resolvermos esse problema, basta declararmos o método depois de sobrescrever o prototype:
PessoaFisica.prototype = newPessoa(); PessoaFisica.prototype.constructor = PessoaFisica; // corrige o ponteiro do construtor PessoaFisica.prototype.dizCpf = function(){ console.log(cpf); };
Agora podemos, ao instanciar uma PessoaFisica, chamar o método fala() que foi declarado na função Pessoa:
var leonardo = newPessoaFisica("Leonardo", "[email protected]", "meucpf"); leonardo.fala(); // Olá, meu nome é Leonardo e meu email é [email protected] leonardo.dizCpf(); //meucpf
Perceba que agora, para instanciar uma PessoaFisica, nós chamamos a função Pessoa duas vezes: ao setar o protótipo da função filha e ao instanciar a função filha.
Esta é a maneira mais comum de se implementar herança em javascript, chamada de Prototype-chaining inheritance.
Confira o resultado da nossa função PessoaFisica herdando Pessoa:
Nós conseguimos o resultado esperado, mas há quem diga que nosso código acabou ficando poluído com o uso dos prototypes: ao utilizarmos a Prototype-chaining Inheritance nós obtemos ganho de performance e diminuição da memória utilizada, mas como consequência nós acabamos danificando esteticamente o nosso código, como expliquei aqui.
2 - Parasitic Combination Inheritance
Nós já vimos que, ao utilizar a Prototype-chaining Inheritance pura, a cada vez que instanciamos uma PessoaFisica, chamamos duas vezes a função Pessoa. Será que não há um modo mais performático de se implementar herança?
Existe sim, mas antes de entendermos como ele é implementado, precisamos entender a função Object.create, escrita por Douglas Crockford, que usaremos em nossa implementação.
Essa é a função, que foi incluída ao ECMAScript 5:
if (typeofObject.create !== 'function') { Object.create = function (o) { functionF() {} F.prototype = o; returnnewF(); }; }
Basicamente o que ela faz é criar uma função construtora F, setar o protótipo desta para o objeto passado como parâmetro e retornar uma instância de F.
Ou seja, ela devolve um objeto vazio que herda (possui a propriedade __proto__ igual ao) objeto passado!
// Cria um objeto vazio com prototype igual ao de Pessoa var pessoa = Object.create(Pessoa.prototype);
Note que, dessa forma, deixamos de dar new em uma Pessoa sem argumentos para criar um objeto vazio com o protótipo de Pessoa, uma cópia!
Mas nós não queremos uma pessoaFisica que é igual a uma pessoa! Queremos que nosso objeto saiba dizer seu CPF, certo? Para isso, você poderia pensar em adicionar direto no objeto retornado:
var pessoaFisica = Object.create(Pessoa.prototype); pessoaFisica.dizCpf = function(){ console.log("meuCPF"); }
Mas assim nós acabamos de perder a vantagem do prototype: vamos definir uma função para cada instância de PessoaFisica.
O que queremos, então, é definir os métodos da PessoaFisica e da Pessoa utilizando seus prototypes e combiná-los de um modo mais enxuto. Seria bacana uma função que encapsulasse todo esse comportamento, onde você passasse a função pai e a função filha e ela se responsabilizasse por fazer a função filha herdar a função pai, ou seja, combinar seus protótipos! ```js
herda(Pessoa, PessoaFisica); // faz PessoaFisica herdar Pessoa
Legal! Então vamos implementar essa função:
```js
var herda = function(mae, filha){ // Faz uma cópia do prototipo da mãe var copiaDaMae = Object.create(mae.prototype);// herda mãe filha.prototype = copiaDaMae;//Ajusta construtor da filha filha.prototype.constructor = filha; }
Agora, para herdarmos Pessoa, basta chamar a função herda passando a PessoaFisica e a Pessoa:
Note que mantemos a estrategia de Constructor Stealling pois, sem ela, perderíamos as validações feitas no construtor do pai(como o if(emailEhValido(email))).
E, em seguida, definir os métodos da PessoaFisica em seu prototype:
Agora, para usar uma PessoaFisica, basta instanciá-la:
var leonardo = newPessoaFisica("Leonardo", "[email protected]", "meucpf"); leonardo.fala(); // Olá, meu nome é Leonardo e meu email é [email protected] leonardo.dizCpf(); //meucpf
Perceba que agora, ao contrário da Prototype-chaining Inheritance, nós só chamamos a função Pessoa uma vez: ao instanciar a PessoaFisica.
Imagine que, em vez de termos uma função construtora e darmos new nela para obtermos um objeto, nós simplesmente retornássemos um objeto utilizando notação literal (esse padrão é explicado aqui):
Mas como vamos implementar herança sem utilizarmos prototypes?
Para isso, vamos utilizar uma estratégia um pouco diferente: em vez de adicionarmos os atributos e métodos nas instâncias de pessoaFisica, vamos fazer a função pessoaFisica() devolver uma pessoa() modificada:
var pessoaFisica = function(nome, email, cpf){ var pessoa = pessoa(nome, email); pessoa.dizCpf = function(){ console.log(cpf); }; return pessoa; }
Uma observação importante: quando utilizada a Funcional Inheritance, em nenhum momento você vai utilizar o operador new, sendo assim, suas funções não deverão ter nada atribuido à referência this. Caso você utilize o this dentro de uma função e a chame sem o uso da palavra chave new, o this apontará para window, criando variáveis globais!
Essa é a estratégia chamada de Functional inheritance.
Para saber mais
Benchmark
Para comparar a performance destes três modos, eu escrevi um benchmark verificando 4 situações para cada um deles.
Grande parte das vezes que eu executei o benchmark, realmente a Parasitic Combination Inheritance foi a mais performática!
Além disso, nos benchmarks onde eu testei a velocidade de instanciação, a Functional Inheritance demorou mais do que o dobro do tempo, comprovando o ganho de performance do uso de prototypes.
O codigo fonte dos benchmarks podem ser encontrados no meu github