Sei que essa resposta está 3 anos atrasada, mas realmente acho que as respostas atuais não fornecem informações suficientes sobre como a herança prototípica é melhor do que a herança clássica .
Primeiro, vamos ver os argumentos mais comuns que os programadores JavaScript afirmam em defesa da herança prototípica (estou usando esses argumentos no conjunto de respostas atual):
- É simples.
- É poderoso.
- Isso leva a um código menor e menos redundante.
- É dinâmico e, portanto, é melhor para linguagens dinâmicas.
Agora, esses argumentos são todos válidos, mas ninguém se deu ao trabalho de explicar o porquê. É como dizer a uma criança que estudar matemática é importante. Claro que é, mas a criança certamente não se importa; e você não pode fazer uma criança gostar de matemática dizendo que é importante.
Penso que o problema com a herança prototípica é que é explicado da perspectiva do JavaScript. Eu amo JavaScript, mas a herança prototípica no JavaScript está errada. Ao contrário da herança clássica, existem dois padrões de herança prototípica:
- O padrão prototípico de herança prototípica.
- O padrão construtor da herança prototípica.
Infelizmente, o JavaScript usa o padrão construtor de herança prototípica. Isso ocorre porque quando o JavaScript foi criado, Brendan Eich (o criador do JS) queria que ele se parecesse com o Java (que tem herança clássica):
E nós estávamos promovendo isso como um irmão mais novo para Java, pois uma linguagem complementar como o Visual Basic era para C ++ nas famílias de idiomas da Microsoft na época.
Isso é ruim porque quando as pessoas usam construtores em JavaScript, elas pensam em construtores herdados de outros construtores. Isto está errado. Na herança prototípica, os objetos herdam de outros objetos. Os construtores nunca entram em cena. É isso que confunde a maioria das pessoas.
Pessoas de linguagens como Java, que tem herança clássica, ficam ainda mais confusas porque, embora os construtores pareçam com classes, eles não se comportam como classes. Como Douglas Crockford afirmou:
Esse indireção pretendia fazer a linguagem parecer mais familiar para programadores treinados de maneira clássica, mas não conseguiu, como podemos ver pela opinião muito baixa que os programadores Java têm do JavaScript. O padrão construtor do JavaScript não atraiu a multidão clássica. Também obscureceu a verdadeira natureza prototípica do JavaScript. Como resultado, existem muito poucos programadores que sabem como usar a linguagem de maneira eficaz.
Aí está. Direto da boca do cavalo.
Verdadeira herança prototípica
A herança prototípica tem tudo a ver com objetos. Objetos herdam propriedades de outros objetos. É tudo o que há para isso. Existem duas maneiras de criar objetos usando herança prototípica:
- Crie um novo objeto.
- Clone um objeto existente e estenda-o.
Nota: O JavaScript oferece duas maneiras de clonar um objeto - delegação e concatenação . A partir de agora, usarei a palavra "clone" para se referir exclusivamente à herança via delegação, e a palavra "cópia" para se referir exclusivamente à herança via concatenação.
Chega de conversa. Vamos ver alguns exemplos. Digamos que eu tenha um círculo de raio 5
:
var circle = {
radius: 5
};
Podemos calcular a área e a circunferência do círculo a partir do seu raio:
circle.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
circle.circumference = function () {
return 2 * Math.PI * this.radius;
};
Agora eu quero criar outro círculo de raio 10
. Uma maneira de fazer isso seria:
var circle2 = {
radius: 10,
area: circle.area,
circumference: circle.circumference
};
No entanto, o JavaScript fornece uma maneira melhor - delegação . A Object.create
função é usada para fazer isso:
var circle2 = Object.create(circle);
circle2.radius = 10;
Isso é tudo. Você acabou de fazer uma herança prototípica em JavaScript. Isso não era simples? Você pega um objeto, clona-o, muda o que precisa e, e pronto - você conseguiu um objeto novo.
Agora você pode perguntar: "Como isso é simples? Toda vez que quero criar um novo círculo, preciso clonar circle
e atribuir manualmente um raio". Bem, a solução é usar uma função para fazer o trabalho pesado para você:
function createCircle(radius) {
var newCircle = Object.create(circle);
newCircle.radius = radius;
return newCircle;
}
var circle2 = createCircle(10);
De fato, você pode combinar tudo isso em um único objeto literal da seguinte maneira:
var circle = {
radius: 5,
create: function (radius) {
var circle = Object.create(this);
circle.radius = radius;
return circle;
},
area: function () {
var radius = this.radius;
return Math.PI * radius * radius;
},
circumference: function () {
return 2 * Math.PI * this.radius;
}
};
var circle2 = circle.create(10);
Herança Prototípica em JavaScript
Se você perceber no programa acima, a create
função cria um clone de circle
, atribui um novo radius
a ele e depois o devolve. É exatamente isso que um construtor faz no JavaScript:
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
Circle.prototype.circumference = function () {
return 2 * Math.PI * this.radius;
};
var circle = new Circle(5);
var circle2 = new Circle(10);
O padrão construtor em JavaScript é o padrão prototípico invertido. Em vez de criar um objeto, você cria um construtor. A new
palavra-chave vincula o this
ponteiro dentro do construtor a um clone do prototype
construtor.
Parece confuso? É porque o padrão do construtor no JavaScript complica desnecessariamente as coisas. É isso que a maioria dos programadores acha difícil de entender.
Em vez de pensar em objetos herdados de outros objetos, eles pensam em construtores herdados de outros construtores e depois ficam completamente confusos.
Há várias outras razões pelas quais o padrão de construtor em JavaScript deve ser evitado. Você pode ler sobre eles no meu blog aqui: Construtores vs Protótipos
Então, quais são os benefícios da herança prototípica sobre a herança clássica? Vamos examinar novamente os argumentos mais comuns e explicar o porquê .
1. Herança prototípica é simples
O CMS declara em sua resposta:
Na minha opinião, o principal benefício da herança prototípica é sua simplicidade.
Vamos considerar o que acabamos de fazer. Criamos um objeto circle
com um raio de 5
. Então nós o clonamos e demos ao raio um clone de 10
.
Portanto, precisamos apenas de duas coisas para fazer a herança prototípica funcionar:
- Uma maneira de criar um novo objeto (por exemplo, literais de objeto).
- Uma maneira de estender um objeto existente (por exemplo
Object.create
).
Em contraste, a herança clássica é muito mais complicada. Na herança clássica, você tem:
- Aulas.
- Objeto.
- Interfaces.
- Classes abstratas.
- Aulas finais.
- Classes base virtuais.
- Construtores.
- Destruidores.
Você entendeu a ideia. O ponto é que a herança prototípica é mais fácil de entender, mais fácil de implementar e mais fácil de raciocinar.
Como Steve Yegge coloca em seu clássico post no blog " Portrait of a N00b ":
Metadados é qualquer tipo de descrição ou modelo de outra coisa. Os comentários no seu código são apenas uma descrição em linguagem natural da computação. O que cria metadados de metadados é que eles não são estritamente necessários. Se eu tenho um cachorro com alguma papelada de pedigree, e eu a perco, ainda tenho um cachorro perfeitamente válido.
No mesmo sentido, as classes são apenas metadados. As classes não são estritamente necessárias para herança. No entanto, algumas pessoas (geralmente n00bs) acham as aulas mais confortáveis para trabalhar. Isso lhes dá uma falsa sensação de segurança.
Bem, também sabemos que tipos estáticos são apenas metadados. Eles são um tipo especializado de comentário direcionado a dois tipos de leitores: programadores e compiladores. Os tipos estáticos contam uma história sobre o cálculo, presumivelmente para ajudar os dois grupos de leitores a entender a intenção do programa. Mas os tipos estáticos podem ser descartados no tempo de execução, porque no final são apenas comentários estilizados. Eles são como a papelada de pedigree: pode tornar um certo tipo de personalidade insegura mais feliz com o cachorro, mas o cachorro certamente não se importa.
Como afirmei anteriormente, as aulas dão às pessoas uma falsa sensação de segurança. Por exemplo, você obtém muitos NullPointerException
s em Java, mesmo quando seu código é perfeitamente legível. Acho que a herança clássica geralmente atrapalha a programação, mas talvez seja apenas Java. Python tem um incrível sistema de herança clássica.
2. Herança prototípica é poderosa
A maioria dos programadores que têm uma formação clássica argumenta que a herança clássica é mais poderosa que a herança prototípica, porque possui:
- Variáveis privadas.
- Herança múltipla.
Esta afirmação é falsa. Já sabemos que o JavaScript suporta variáveis privadas por meio de fechamentos , mas e a herança múltipla? Objetos em JavaScript possuem apenas um protótipo.
A verdade é que a herança prototípica suporta a herança de vários protótipos. Herança prototípica significa simplesmente um objeto herdado de outro objeto. Na verdade, existem duas maneiras de implementar a herança prototípica :
- Delegação ou herança diferencial
- Clonagem ou herança concatenativa
Sim JavaScript permite apenas que objetos sejam delegados a outro objeto. No entanto, permite copiar as propriedades de um número arbitrário de objetos. Por exemplo, _.extend
faz exatamente isso.
Claro que muitos programadores não consideram que isso seja verdade herança porque instanceof
e isPrototypeOf
dizer o contrário. No entanto, isso pode ser facilmente remediado armazenando uma matriz de protótipos em cada objeto que herda de um protótipo por concatenação:
function copyOf(object, prototype) {
var prototypes = object.prototypes;
var prototypeOf = Object.isPrototypeOf;
return prototypes.indexOf(prototype) >= 0 ||
prototypes.some(prototypeOf, prototype);
}
Portanto, a herança prototípica é tão poderosa quanto a herança clássica. Na verdade, é muito mais poderoso que a herança clássica, porque na herança prototípica você pode escolher manualmente quais propriedades copiar e quais propriedades omitir de diferentes protótipos.
Na herança clássica, é impossível (ou pelo menos muito difícil) escolher quais propriedades você deseja herdar. Eles usam classes base virtuais e interfaces para resolver o problema do diamante .
No JavaScript, porém, você provavelmente nunca ouvirá falar do problema dos diamantes, porque pode controlar exatamente quais propriedades deseja herdar e de quais protótipos.
3. Herança prototípica é menos redundante
Este ponto é um pouco mais difícil de explicar porque a herança clássica não leva necessariamente a um código mais redundante. De fato, a herança, seja clássica ou prototípica, é usada para reduzir a redundância no código.
Um argumento pode ser que a maioria das linguagens de programação com herança clássica é digitada estaticamente e requer que o usuário declare explicitamente os tipos (ao contrário de Haskell, que possui tipagem estática implícita). Portanto, isso leva a um código mais detalhado.
Java é notório por esse comportamento. Lembro-me claramente de Bob Nystrom mencionando a seguinte anedota em seu post sobre Pratt Parsers :
Você precisa amar o nível de burocracia do Java "assine-o em quadruplicado" aqui.
Mais uma vez, acho que é apenas porque o Java é péssimo.
Um argumento válido é que nem todos os idiomas que têm herança clássica suportam herança múltipla. Novamente Java vem à mente. Sim, o Java tem interfaces, mas isso não é suficiente. Às vezes, você realmente precisa de herança múltipla.
Como a herança prototípica permite herança múltipla, o código que requer herança múltipla é menos redundante se escrito usando herança prototípica, em vez de em um idioma que tenha herança clássica, mas não herança múltipla.
4. Herança prototípica é dinâmica
Uma das vantagens mais importantes da herança de protótipo é que você pode adicionar novas propriedades aos protótipos após a criação. Isso permite que você adicione novos métodos a um protótipo que será automaticamente disponibilizado a todos os objetos que delegam a esse protótipo.
Isso não é possível na herança clássica porque uma vez que uma classe é criada, você não pode modificá-la em tempo de execução. Essa é provavelmente a maior vantagem da herança prototípica sobre a herança clássica, e deveria estar no topo. No entanto, eu gosto de guardar o melhor para o fim.
Conclusão
A herança prototípica é importante. É importante educar os programadores de JavaScript sobre por que abandonar o padrão construtor da herança prototípica em favor do padrão prototípico da herança prototípica.
Precisamos começar a ensinar JavaScript corretamente e isso significa mostrar aos novos programadores como escrever código usando o padrão prototípico em vez do padrão construtor.
Não só será mais fácil explicar a herança prototípica usando o padrão prototípico, como também criará melhores programadores.
Se você gostou desta resposta, também deve ler meu post no blog " Por que a herança prototípica é importante ". Confie em mim, você não ficará desapontado.