Reaproveitando código com JavaScript: herança e protótipos

Reaproveitando código com JavaScript: herança e protótipos
leonardo.wolter
leonardo.wolter

Compartilhe

Introdução

Quando estamos desenvolvendo um sistema orientado a objetos, é comum termos uma certa preocupação com repetição de código.

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
Banner de divulgação da Imersão IA da Alura em colaboração com o Google. Mergulhe em Inteligência artificial com a Alura e o Google. Serão cinco aulas gratuitas para você aprender a usar IA na prática e desenvolver habilidades essenciais para o mercado de trabalho. Inscreva-se gratuitamente agora!

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 :

 var Pessoa = 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 = new Pessoa("Leonardo", "emailinvalido"); 

Por estarmos utilizando uma função construtora, fica fácil resolver esse problema:

 var Pessoa = function(nome, email){ this.nome = nome; if(emailEhValido(email)) this.email = email }; 

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:

 var PessoaFisica = function(nome, email, cpf){ this.nome = nome; this.email = email; this.cpf = cpf; };

PessoaFisica.prototype.dizCpf = function(){ console.log(this.cpf); }; 

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.

Essa técnica é chamada de Constructor Stealing :

 var PessoaFisica = function(nome, email, cpf){ Pessoa.call(this, nome, email); this.cpf = cpf; };

PessoaFisica.prototype.dizCpf = function(){ console.log(this.cpf); }; 

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 = new Pessoa(); 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 = new Pessoa(); 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 = new PessoaFisica("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:

 var Pessoa = function(nome, email) { this.nome = nome;

if(emailEhValido(email)) this.email = email; }

Pessoa.prototype.fala = function(){ console.log("Olá, meu nome é "+this.nome+" e meu email é "+this.email); }

var PessoaFisica = function(nome, email, cpf){ Pessoa.call(this, nome, email); this.cpf = cpf; };

PessoaFisica.prototype = new Pessoa(); PessoaFisica.prototype.constructor = PessoaFisica; PessoaFisica.prototype.dizCpf = function(){ console.log(this.cpf); };

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 (typeof Object.create !== 'function') { Object.create = function (o) { function F() {} F.prototype = o; return new F(); }; } 

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:

 var PessoaFisica = function(nome, email, cpf){ Pessoa.call(this, nome, email); this.cpf = cpf; };

herda(Pessoa, PessoaFisica); 

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:

 var PessoaFisica = function(nome, email, cpf){ Pessoa.call(this, nome, email); this.cpf = cpf; };

herda(Pessoa, PessoaFisica);

PessoaFisica.prototype.dizCpf = function(){ console.log(this.cpf); }; 

Agora, para usar uma PessoaFisica, basta instanciá-la:

 var leonardo = new PessoaFisica("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.

Esta estratégia é descrita no livro Professional JavaScript for Web Developers (de Nicholas C. Zakas) como Parasitic Combination, um modo de implementar herança que o próprio Zakas descreveu como "the most optimal inheritance paradigm".

3 - Functional Inheritance

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.

Os resultados podem ser observados neste gist

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

Veja outros artigos sobre Front-end