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

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

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", "leonardo.wolter@caelum.com.br", "meucpf");
leonardo.fala(); // Olá, meu nome é Leonardo e meu email é leonardo.wolter@caelum.com.br
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!

herda(Pessoa, PessoaFisica); // faz PessoaFisica herdar Pessoa

Legal! Então vamos implementar essa função:

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", "leonardo.wolter@caelum.com.br", "meucpf");
leonardo.fala(); // Olá, meu nome é Leonardo e meu email é leonardo.wolter@caelum.com.br
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

10 Comentários

  1. Luiz Corte Real 26/05/2014 at 13:14 #

    Ótimo post, Léo!

    Só de curiosidade, peguei seu benchmark e portei para o navegador. Para quem quiser dar uma olhada:

    https://github.com/luiz/javascriptInheritance

    Não atualizei o README ainda com os resultados, mas dá para você mesmo rodar no seu navegador. Só entrar em http://luiz.github.io/javascriptInheritance/runBenchs.html (resultados no console)

  2. Leonardo Wolter 26/05/2014 at 13:26 #

    Muito obrigado, Luiz!

    Em geral os resultados foram bem parecidos aqui no meu navegador, o unico resultado meio bizarro foi o do childMethod. No nodeJS o parasitic foi mais rapido e no navegador o pseudo foi visivelmente mais rapido haha.

    Obrigado pela contribuição!

  3. Mário Braga 02/06/2014 at 11:30 #

    Fico feliz em saber que o pessoal contribui de forma objetiva com a galera desenvolvedora, liberando/compartilhando informações e experiências.

    Parabéns.

  4. Leonardo Wolter 02/06/2014 at 14:02 #

    O prazer é nosso, Mário, muito obrigado!

  5. Fabio Santos 18/07/2014 at 15:18 #

    Excelente post. Muito bem detalhado.

  6. Léo Costa 12/01/2015 at 15:57 #

    Muito bom o post, Léo. Esclareceu completamente minhas dúvidas sobre herança em JavaScript. Obrigado por compartilhar (no dia do meu aniversário, kkk).

  7. Leonardo Wolter 12/01/2015 at 16:07 #

    Muito obrigado Léo e Fabio!

  8. Ted k' 30/04/2015 at 15:28 #

    Achei uma ótima explicação, uma das melhores que já encontrei, parabéns cara!

  9. Caio Cutrim 01/07/2015 at 21:37 #

    Nas minhas andanças pelas interwebs vim parar aqui haha já tinha lido umas apostilas da caelum e venho aproveitar este comentário para agradecer a iniciativa de compartilhamento de conhecimento, isso é muito cativante! Mas me veio uma dúvida, como você fez esse benchmark no node? Eu procurei umas coisas, mas não etendi direito.

  10. Marcos 12/10/2016 at 12:27 #

    Tenho uma dúvida:
    Qual seria o problema de não ter essa linha de código:
    PessoaFisica.prototype.constructor = PessoaFisica; // corrige o ponteiro do construtor

    Pra que serve esse ponteiro do construtor?
    Pois eu fiz um teste aqui sem essa linha e aparentemente o código funcionava tranquilo.

Deixe uma resposta