Benefícios da herança prototípica em relação à clássica?


271

Por fim, parei de arrastar os pés durante todos esses anos e decidi aprender JavaScript "corretamente". Um dos elementos mais impressionantes do design de idiomas é a implementação de herança. Tendo experiência em Ruby, fiquei muito feliz ao ver fechamentos e digitação dinâmica; mas, por toda a minha vida, não consigo descobrir quais benefícios devem ser obtidos com as instâncias de objetos usando outras instâncias para herança.



Respostas:


560

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):

  1. É simples.
  2. É poderoso.
  3. Isso leva a um código menor e menos redundante.
  4. É 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:

  1. O padrão prototípico de herança prototípica.
  2. 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:

  1. Crie um novo objeto.
  2. 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.createfunçã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 circlee 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 createfunção cria um clone de circle, atribui um novo radiusa 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 newpalavra-chave vincula o thisponteiro dentro do construtor a um clone do prototypeconstrutor.

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 circlecom 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:

  1. Uma maneira de criar um novo objeto (por exemplo, literais de objeto).
  2. 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:

  1. Aulas.
  2. Objeto.
  3. Interfaces.
  4. Classes abstratas.
  5. Aulas finais.
  6. Classes base virtuais.
  7. Construtores.
  8. 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 NullPointerExceptions 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:

  1. Variáveis ​​privadas.
  2. 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 :

  1. Delegação ou herança diferencial
  2. 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, _.extendfaz exatamente isso.

Claro que muitos programadores não consideram que isso seja verdade herança porque instanceofe isPrototypeOfdizer 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.


33
Embora eu entenda de onde você é e concorde que a herança prototípica é muito útil, acho que assumindo que "a herança prototípica é melhor que a herança clássica", você já está destinado a falhar. Para lhe dar uma perspectiva, minha biblioteca jTypes é uma biblioteca de herança clássica para JavaScript. Sendo um cara que demorou um pouco para fazer isso, ainda vou sentar aqui e dizer que a herança prototípica é incrível e muito útil. Mas é simplesmente apenas uma ferramenta entre muitas que um programador possui. Ainda existem muitas desvantagens na herança prototípica.
redline

7
Eu concordo completamente com o que você disse. Eu sinto que muitos programadores rejeitam o JavaScript por falta de herança clássica ou o denunciam como uma linguagem simples e burra. Na verdade, é um conceito extremamente poderoso, eu concordo, e muitos programadores devem aceitá-lo e aprendê-lo. Com isso dito, também sinto que há um número significativo de desenvolvedores em JavaScript que se opõem a qualquer forma de herança clássica, todos juntos em JavaScript, quando eles realmente não têm base para seus argumentos. Ambos são igualmente poderosos por si mesmos e igualmente úteis.
redline

9
Bem, essa é a sua opinião, mas continuarei discordando e acho que a crescente popularidade de coisas como CoffeeScript e TypeScript mostra que há uma grande comunidade de desenvolvedores que gostariam de utilizar essa funcionalidade quando apropriado. Como você disse, o ES6 adiciona açúcar sintático, mas ainda não oferece a extensividade dos jTypes. Em uma nota lateral, eu não sou o responsável pelo seu voto negativo. Embora eu não concorde com você, não acho que você tenha uma resposta ruim. Você foi muito meticuloso.
redline

25
Você está usando muito o clone de palavras , o que está simplesmente errado. Object.createestá criando um novo objeto com o protótipo especificado. Sua escolha de palavras dá a impressão de que o protótipo é clonado.
Pavel Horal

7
@Aadit: realmente não há necessidade de ser tão defensivo. Sua resposta é muito detalhada e merece votos. Eu não estava sugerindo que "vinculado" deveria ser um substituto para "clone", mas descreve de maneira mais apropriada a conexão entre um objeto e o protótipo do qual ele herda, independentemente de você afirmar sua própria definição do termo "clone" " ou não. Mude ou não, é inteiramente sua escolha.
Andy E

42

Permita-me realmente responder à pergunta em linha.

A herança de protótipo possui as seguintes virtudes:

  1. É mais adequado para linguagens dinâmicas porque a herança é tão dinâmica quanto o ambiente em que está. (A aplicabilidade ao JavaScript deve ser óbvia aqui.) Isso permite que você faça coisas rapidamente rapidamente, como personalizar classes sem grandes quantidades de código de infraestrutura .
  2. É mais fácil implementar um esquema de objeto de prototipagem do que os esquemas clássicos de dicotomia de classe / objeto.
  3. Isso elimina a necessidade de arestas nítidas complexas ao redor do modelo de objeto, como "metaclasses" (nunca gostei da metaclasse que eu gostei ... desculpe!) Ou "autovalores" ou algo parecido.

No entanto, possui as seguintes desvantagens:

  1. A verificação de tipo em uma linguagem de protótipo não é impossível, mas é muito, muito difícil. A maioria das "verificações de tipo" de idiomas prototípicos são verificações no estilo de "digitação de pato" em tempo de execução. Isso não é adequado para todos os ambientes.
  2. É igualmente difícil fazer coisas como otimizar o envio de métodos por análise estática (ou, muitas vezes, até dinâmica!). Ele pode (sublinho: pode ) ser muito ineficiente muito facilmente.
  3. Da mesma forma, a criação de objetos pode ser (e geralmente é) muito mais lenta em uma linguagem de prototipagem do que em um esquema de dicotomia de classe / objeto mais convencional.

Acho que você pode ler nas entrelinhas acima e apresentar as vantagens e desvantagens correspondentes dos esquemas tradicionais de classe / objeto. É claro que há mais em cada área, então deixarei o resto para outras pessoas respondendo.


1
Ei, olha, uma resposta concisa que não faz fanboy. Realmente gostaria que essa fosse a melhor resposta para a pergunta.
siliconrockstar

Atualmente, temos compiladores dinâmicos Just-in-time que podem compilar o código enquanto o código está em execução, criando diferentes partes de código para cada seção. O JavaScript é realmente mais rápido que o Ruby ou o Python que usam classes clássicas por causa disso, mesmo que você use protótipos, porque muito trabalho foi feito para otimizá-lo.
aoeu256 9/07/19

28

Na OMI, o principal benefício da herança prototípica é sua simplicidade.

A natureza prototípica da linguagem pode confundir as pessoas que são classicamente treinadas, mas acontece que, na verdade, esse é um conceito realmente simples e poderoso, a herança diferencial .

Você não precisa fazer a classificação , seu código é menor, menos redundante, os objetos herdam de outros objetos mais gerais.

Se você pensa em protótipo , logo perceberá que não precisa de aulas ...

A herança prototípica será muito mais popular em um futuro próximo, a especificação ECMAScript 5th Edition introduziu o Object.createmétodo, que permite produzir uma nova instância de objeto que herda de outra de uma maneira realmente simples:

var obj = Object.create(baseInstance);

Esta nova versão do padrão está sendo implementada por todos os fornecedores de navegadores, e acho que começaremos a ver uma herança prototípica mais pura ...


11
"seu código é menor, menos redundante ...", por quê? Eu dei uma olhada no link da Wikipedia para "herança diferencial" e não há nada que apóie essas afirmações. Por que a herança clássica resultaria em código maior e mais redundante?
20912 Noel Abrahams

4
Exatamente, eu concordo com Noel. A herança prototípica é simplesmente uma maneira de realizar um trabalho, mas isso não torna o caminho certo. Diferentes ferramentas serão executadas de maneiras diferentes para diferentes trabalhos. A herança prototípica tem seu lugar. É um conceito extremamente poderoso e simples. Com isso dito, a falta de suporte ao verdadeiro encapsulamento e polimorfismo coloca o JavaScript em uma desvantagem significativa. Essas metodologias existem há muito mais tempo que o JavaScript e são válidas em seus princípios. Portanto, pensar que o protótipo é "melhor" é apenas a mentalidade errada.
redline

1
Você pode simular herança baseada em classe usando herança baseada em protótipo, mas não vice-versa. Esse pode ser um bom argumento. Também vejo o encapsulamento mais como uma convenção do que o recurso de idioma (geralmente você pode quebrar o encapsulamento por meio de reflexão). Em relação ao polimorfismo - tudo o que você ganha é não ter que escrever algumas condições simples "se" ao verificar os argumentos do método (e um pouco de velocidade se o método de destino for resolvido durante a compilação). Não há desvantagens reais de JavaScript aqui.
Pavel Horal

Protótipos são impressionantes, IMO. Estou pensando em criar uma linguagem funcional como Haskell ... mas, em vez de criar abstrações, basharei tudo em protótipos. Em vez de generalizar soma e fatorial para "dobrar", você deve o protótipo da função soma e substituir o + por * e 0 por 1 para criar o produto. Vou explicar "Mônadas" como protótipos que substituem "then" de promessas / retornos de chamada com flatMap como sinônimo de "then". Eu acho que protótipos podem ajudar a trazer programação funcional para as massas.
aoeu256 16/08/19

11

Realmente não há muito o que escolher entre os dois métodos. A idéia básica a entender é que, quando o mecanismo JavaScript recebe uma propriedade de um objeto para leitura, ele primeiro verifica a instância e, se essa propriedade estiver ausente, verifica a cadeia de protótipos. Aqui está um exemplo que mostra a diferença entre prototípico e clássico:

Prototipado

var single = { status: "Single" },
    princeWilliam = Object.create(single),
    cliffRichard = Object.create(single);

console.log(Object.keys(princeWilliam).length); // 0
console.log(Object.keys(cliffRichard).length); // 0

// Marriage event occurs
princeWilliam.status = "Married";

console.log(Object.keys(princeWilliam).length); // 1 (New instance property)
console.log(Object.keys(cliffRichard).length); // 0 (Still refers to prototype)

Clássico com métodos de instância (Ineficiente porque cada instância armazena sua própria propriedade)

function Single() {
    this.status = "Single";
}

var princeWilliam = new Single(),
    cliffRichard = new Single();

console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 1

Clássico eficiente

function Single() {
}

Single.prototype.status = "Single";

var princeWilliam = new Single(),
    cliffRichard = new Single();

princeWilliam.status = "Married";

console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 0
console.log(cliffRichard.status); // "Single"

Como você pode ver, como é possível manipular o protótipo de "classes" declaradas no estilo clássico, não há realmente nenhum benefício em usar a herança prototípica. É um subconjunto do método clássico.


2
Olhando para outras respostas e recursos sobre este tópico, sua resposta parece estar declarando: "Herança prototípica é um subconjunto do açúcar sintático adicionado ao JavaScript para permitir o aparecimento da herança clássica". O OP parece estar perguntando sobre os benefícios da herança prototípica em JS sobre a herança clássica em outros idiomas, em vez de comparar técnicas de instanciação no JavaScript.
um fader escura

2

Desenvolvimento Web: Herança Prototípica vs. Herança Clássica

http://chamnapchhorn.blogspot.com/2009/05/prototypal-inheritance-vs-classical.html

Classical vs herança prototípica - Stack Overflow em Português

Herança prototípica clássica versus


20
Eu acho que é melhor resumir o conteúdo dos links em vez de colar o link (algo que eu mesmo costumava fazer), a menos que seja outro link SO. Isso ocorre porque os links / sites são desativados e você perde a resposta à pergunta, e isso potencialmente afeta os resultados da pesquisa.
James Westgate

O primeiro link não responde à pergunta de por que a herança prototípica? Simplesmente o descreve.
viebel
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.