web-dev-qa-db-fra.com

Les avantages de l'héritage prototypique par rapport à la technique classique?

Alors, j'ai finalement arrêté de traîner les pieds toutes ces années et j'ai décidé d'apprendre le JavaScript "correctement". L'un des éléments les plus frappants de la conception des langages est sa mise en œuvre de l'héritage. Ayant de l'expérience dans Ruby, j'étais vraiment heureux de voir les fermetures et la dactylographie dynamique; mais pour ma vie, je ne peux pas comprendre quels sont les avantages des instances d'objet utilisant d'autres instances pour l'héritage.

263
Pierreten

Je sais que cette réponse a 3 ans de retard, mais je pense vraiment que les réponses actuelles ne fournissent pas assez d'informations sur la façon dont l'héritage prototypique est meilleur que l'héritage classique .

Voyons d'abord les arguments les plus courants que les programmeurs JavaScript déclarent pour défendre l'héritage prototypique (je prends ces arguments dans le pool de réponses actuel):

  1. C'est simple.
  2. C'est puissant.
  3. Cela conduit à un code plus petit et moins redondant.
  4. C'est dynamique et donc c'est mieux pour les langages dynamiques.

Maintenant, ces arguments sont tous valables, mais personne n’a pris la peine d’expliquer pourquoi. C'est comme dire à un enfant qu'il est important d'étudier les mathématiques. Bien sûr que si, mais l’enfant ne s’en soucie certainement pas; et vous ne pouvez pas faire un enfant comme Maths en disant que c'est important.

Je pense que le problème avec l'héritage prototype est qu'il est expliqué du point de vue de JavaScript. J'adore JavaScript, mais l'héritage prototype en JavaScript est faux. Contrairement à l'héritage classique, il existe deux modèles d'héritage prototypique:

  1. Le modèle prototypal de l'héritage prototypique.
  2. Le modèle de constructeur de l'héritage prototypique.

Malheureusement, JavaScript utilise le modèle de constructeur d'héritage prototypique. En effet, lors de la création de JavaScript, Brendan Eich (le créateur de JS) souhaitait que cela ressemble à Java (qui possède un héritage classique):

Et nous le défendions comme un petit frère de Java, car un langage complémentaire comme Visual Basic était le C++ dans les familles de langues de Microsoft à l’époque.

C'est dommage car lorsque les utilisateurs utilisent des constructeurs en JavaScript, ils pensent aux constructeurs héritant d'autres constructeurs. C'est faux. Dans l'héritage prototype, les objets héritent d'autres objets. Les constructeurs n'interviennent jamais. C'est ce qui déroute la plupart des gens.

Les personnes de langages comme Java, qui possède un héritage classique, sont encore plus confuses car, bien que les constructeurs ressemblent à des classes, ils ne se comportent pas comme des classes. Comme Douglas Crockford a déclaré:

Cette indirection visait à rendre le langage plus familier aux programmeurs de formation classique, mais elle n’a pas réussi à le faire, comme le montre l’opinion très basse Java ont JavaScript). Le modèle constructeur de JavaScript a Il n’a pas attiré la foule classique, il a également occulté la véritable nature prototypique de JavaScript, de sorte que très peu de programmeurs savent utiliser efficacement ce langage.

Voilà. Tout droit de la bouche du cheval.

Véritable héritage prototypique

L'héritage prototypique est tout au sujet des objets. Les objets héritent des propriétés d'autres objets. C'est tout ce qu'on peut en dire. Il existe deux manières de créer des objets en utilisant un héritage prototypique:

  1. Créez un nouvel objet.
  2. Cloner un objet existant et l'étendre.

Remarque: JavaScript offre deux méthodes pour cloner un objet - délégation et enchaînement . J'utiliserai désormais le mot "clone" pour désigner exclusivement l'héritage par délégation, et le mot "copie" pour désigner exclusivement l'héritage par concaténation.

Assez parlé. Voyons quelques exemples. Disons que j'ai un cercle de rayon 5:

var circle = {
    radius: 5
};

Nous pouvons calculer l'aire et la circonférence du cercle à partir de son rayon:

circle.area = function () {
    var radius = this.radius;
    return Math.PI * radius * radius;
};

circle.circumference = function () {
    return 2 * Math.PI * this.radius;
};

Maintenant, je veux créer un autre cercle de rayon 10. Une façon de le faire serait:

var circle2 = {
    radius: 10,
    area: circle.area,
    circumference: circle.circumference
};

Cependant, JavaScript offre un meilleur moyen - délégation . Le Object.create est utilisé pour faire ceci:

var circle2 = Object.create(circle);
circle2.radius = 10;

C'est tout. Vous venez de faire l'héritage prototype en JavaScript. N'était-ce pas simple? Vous prenez un objet, le clonez, changez ce que vous voulez, et hop, vous avez un nouvel objet.

Maintenant, vous pourriez demander: "Comment est-ce si simple? Chaque fois que je veux créer un nouveau cercle, je dois cloner circle et lui attribuer manuellement un rayon". La solution consiste à utiliser une fonction permettant de faire le gros du travail pour vous:

function createCircle(radius) {
    var newCircle = Object.create(circle);
    newCircle.radius = radius;
    return newCircle;
}

var circle2 = createCircle(10);

En fait, vous pouvez combiner tout cela en un seul objet littéral, comme suit:

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

Héritage prototypique en JavaScript

Si vous remarquez que, dans le programme ci-dessus, la fonction create crée un clone de circle, lui attribue un nouveau radius puis le renvoie. C'est exactement ce que fait un constructeur en 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);

Le modèle constructeur en JavaScript est le modèle prototype inversé. Au lieu de créer un objet, vous créez un constructeur. Le mot clé new lie le pointeur this à l'intérieur du constructeur à un clone du prototype du constructeur.

Cela semble déroutant? C'est parce que le modèle de constructeur en JavaScript complique inutilement les choses. C'est ce que la plupart des programmeurs trouvent difficile à comprendre.

Au lieu de penser à des objets héritant d’autres objets, ils pensent à des constructeurs héritant d’autres constructeurs et deviennent alors complètement confus.

Il y a bien d'autres raisons pour lesquelles le modèle de constructeur en JavaScript doit être évité. Vous pouvez lire à ce sujet dans mon article de blog ici: Constructeurs vs Prototypes


Alors, quels sont les avantages de l'héritage prototype par rapport à l'héritage classique? Reprenons les arguments les plus courants et expliquons pourquoi.

1. L'héritage prototypique est simple

[~ # ~] cms [~ # ~] indique dans sa réponse:

À mon avis, le principal avantage de l'héritage prototype est sa simplicité.

Considérons ce que nous venons de faire. Nous avons créé un objet circle qui avait un rayon de 5. Puis nous l'avons cloné et avons donné au clone un rayon de 10.

Par conséquent, nous n'avons besoin que de deux choses pour que l'héritage prototype fonctionne:

  1. Un moyen de créer un nouvel objet (par exemple, les littéraux d’objet).
  2. Un moyen d’étendre un objet existant (par exemple, Object.create).

En revanche, l'héritage classique est beaucoup plus compliqué. En héritage classique, vous avez:

  1. Des classes.
  2. Objet.
  3. Interfaces.
  4. Classes abstraites.
  5. Classes finales.
  6. Classes de base virtuelles.
  7. Constructeurs.
  8. Destructeurs.

Vous avez eu l'idée. Le fait est que l'héritage prototype est plus facile à comprendre, à mettre en œuvre et à raisonner.

Comme Steve Yegge le dit dans son billet de blog classique " Portrait d’un N00b ":

Les métadonnées sont n'importe quel type de description ou de modèle d'autre chose. Les commentaires dans votre code sont simplement une description du calcul en langage naturel. Ce qui rend les méta-données de métadonnées, c'est que ce n'est pas strictement nécessaire. Si j'ai un chien avec des documents de généalogie et que je perds ces documents, j'ai toujours un chien parfaitement valide.

Dans le même sens, les classes ne sont que des méta-données. Les classes ne sont pas strictement requises pour l'héritage. Cependant, certaines personnes (généralement n00bs) trouvent les cours plus confortables. Cela leur donne un faux sentiment de sécurité.

Nous savons également que les types statiques ne sont que des métadonnées. Ce sont des commentaires spécialisés destinés à deux types de lecteurs: les programmeurs et les compilateurs. Les types statiques racontent une histoire sur le calcul, probablement pour aider les deux groupes de lecteurs à comprendre l'intention du programme. Mais les types statiques peuvent être supprimés à l'exécution, car ils ne sont finalement que des commentaires stylisés. Ils ressemblent à de la paperasse généalogique: cela rendrait plus heureux certains types de personnalité peu sûrs de leur chien, mais celui-ci s'en fiche.

Comme je l'ai dit précédemment, les classes donnent aux gens un faux sentiment de sécurité. Par exemple, vous obtenez trop de NullPointerExceptions dans Java même si votre code est parfaitement lisible. Je trouve que l'héritage classique gêne généralement la programmation, mais peut-être que ce n'est que Java. = Python possède un incroyable système d’héritage classique.

2. L'héritage prototypique est puissant

La plupart des programmeurs issus du monde classique soutiennent que l'héritage classique est plus puissant que l'héritage prototype, car il possède:

  1. Variables privées.
  2. Héritage multiple.

Cette affirmation est fausse. Nous savons déjà que JavaScript supporte les variables privées via des fermetures , mais qu'en est-il de l'héritage multiple? Les objets en JavaScript n'ont qu'un seul prototype.

La vérité est que l'héritage prototypique prend en charge l'héritage de plusieurs prototypes. L'héritage prototypique signifie simplement qu'un objet hérite d'un autre objet. Il existe en réalité deux méthodes pour implémenter l'héritage prototype :

  1. Délégation ou héritage différentiel
  2. Clonage ou héritage concaténatif

Oui JavaScript permet uniquement aux objets de déléguer à un autre objet. Cependant, cela vous permet de copier les propriétés d'un nombre arbitraire d'objets. Par exemple _.extend ne fait que cela.

Bien sûr, beaucoup de programmeurs ne considèrent pas cela comme un véritable héritage, car instanceof et isPrototypeOf dire le contraire. Cependant, cela peut être facilement résolu en stockant un tableau de prototypes sur chaque objet qui hérite d'un prototype via la concaténation:

function copyOf(object, prototype) {
    var prototypes = object.prototypes;
    var prototypeOf = Object.isPrototypeOf;
    return prototypes.indexOf(prototype) >= 0 ||
        prototypes.some(prototypeOf, prototype);
}

Par conséquent, l'héritage prototype est aussi puissant que l'héritage classique. En réalité, il est beaucoup plus puissant que l'héritage classique car, dans l'héritage prototype, vous pouvez choisir quelles propriétés copier et quelles propriétés doivent être omises de différents prototypes.

Dans l'héritage classique, il est impossible (ou du moins très difficile) de choisir les propriétés dont vous souhaitez hériter. Ils utilisent des classes de base virtuelles et des interfaces pour résoudre le problème du diamant .

En JavaScript, toutefois, vous n'entendrez probablement jamais parler du problème des diamants, car vous pouvez contrôler exactement les propriétés dont vous souhaitez hériter et de quels prototypes.

3. L'héritage prototypique est moins redondant

Ce point est un peu plus difficile à expliquer car l'héritage classique ne conduit pas nécessairement à un code plus redondant. En fait, l'héritage, qu'il soit classique ou prototype, est utilisé pour réduire la redondance dans le code.

Un argument pourrait être que la plupart des langages de programmation avec héritage classique sont typés statiquement et obligent l'utilisateur à déclarer explicitement les types (contrairement à Haskell qui utilise un typage statique implicite). Cela conduit donc à un code plus détaillé.

Java est connu pour ce comportement. Je me rappelle distinctement Bob Nystrom mentionnant l'anecdote suivante dans son billet de blog à propos de Pratt Parsers :

Tu dois aimer le niveau de bureaucratie "s'il vous plait, signez-le en quadruplicate" de Java ici.

Encore une fois, je pense que c'est uniquement parce que Java est vraiment nul.

Un argument valable est que toutes les langues ayant un héritage classique ne prennent pas en charge l'héritage multiple. Encore une fois Java vient à l’esprit. Oui Java possède des interfaces, mais cela ne suffit pas. Parfois, vous avez réellement besoin d’un héritage multiple.

Etant donné que l'héritage prototypique autorise l'héritage multiple, le code nécessitant l'héritage multiple est moins redondant s'il est écrit avec l'héritage prototypique plutôt que dans un langage qui possède un héritage classique mais pas d'héritage multiple.

4. L'héritage prototypique est dynamique

L'un des avantages les plus importants de l'héritage de prototypes est que vous pouvez ajouter de nouvelles propriétés aux prototypes après leur création. Cela vous permet d’ajouter de nouvelles méthodes à un prototype qui sera automatiquement mis à la disposition de tous les objets qui délégueront à ce prototype.

Ceci n'est pas possible dans l'héritage classique, car une fois qu'une classe est créée, vous ne pouvez pas la modifier au moment de l'exécution. C'est probablement le plus gros avantage de l'héritage prototype par rapport à l'héritage classique, et il aurait dû être au top. Cependant, j'aime bien garder le meilleur pour la fin.

Conclusion

L'héritage prototypique est important. Il est important d'éduquer les programmeurs JavaScript sur les raisons pour lesquelles ils abandonnent le modèle constructeur d'héritage prototypique au profit du modèle prototypal d'héritage prototypique.

Nous devons commencer à enseigner le JavaScript correctement, ce qui signifie montrer aux nouveaux programmeurs comment écrire du code en utilisant le modèle prototype au lieu du modèle constructeur.

Non seulement il sera plus facile d'expliquer l'héritage de prototypes en utilisant le modèle de prototypes, mais cela rendra également de meilleurs programmeurs.

Si vous avez aimé cette réponse, vous devriez également lire mon billet de blog sur " Pourquoi l'héritage prototypal est important ". Croyez-moi, vous ne serez pas déçu.

530
Aadit M Shah

Permettez-moi de répondre à la question en ligne.

L'héritage prototype a les vertus suivantes:

  1. Il convient mieux aux langages dynamiques car l'héritage est aussi dynamique que l'environnement dans lequel il se trouve. (L'applicabilité à JavaScript devrait être évidente ici.) Cela vous permet de faire des choses rapidement, comme en personnalisant des classes sans énormément de code d'infrastructure. .
  2. Il est plus facile de mettre en œuvre un schéma d’objet de prototypage que les schémas classiques de dichotomie classe/objet.
  3. Cela élimine le besoin d'arêtes vives complexes autour du modèle d'objet, comme les "métaclasses" (je n'ai jamais aimé la métaclasse que j'ai aimée ... désolé!) Ou les "valeurs propres", etc.

Il présente cependant les inconvénients suivants:

  1. Vérifier le langage d'un prototype n'est pas impossible, mais c'est très, très difficile. La plupart des "vérifications de type" de langages prototypiques sont des vérifications de style "à la frappe de canard" au moment de l'exécution. Cela ne convient pas à tous les environnements.
  2. De même, il est difficile de faire des choses telles que l'optimisation de l'envoi de méthodes par analyse statique (ou souvent même dynamique!). Cela peut (je souligne: peut) est très inefficace très facilement.
  3. De même, la création d'objets peut être (et est généralement) beaucoup plus lente dans un langage de prototypage que dans un schéma de dichotomie classe/objet plus conventionnel.

Je pense que vous pouvez lire entre les lignes ci-dessus et trouver les avantages et inconvénients correspondants des schémas de classe/objet traditionnels. Bien sûr, il y en a plus dans chaque domaine, je vais donc laisser le reste à d'autres personnes.

OMI, le principal avantage de l'héritage prototype est sa simplicité.

La nature prototypique de la langue peut semer la confusion chez les personnes qui sont classiquement formées, mais il s’avère qu’il s’agit en réalité d’un vraiment concept simple et puissant, héritage différentiel .

Vous n'avez pas besoin de faire une classification , votre code est plus petit, moins redondant, les objets héritent d'autres objets plus généraux.

Si vous pensez de manière prototypique , vous remarquerez bientôt que vous n'avez pas besoin de classes ...

L’héritage prototype sera beaucoup plus populaire dans un avenir proche, la spécification ECMAScript 5th Edition introduit le Object.create méthode, qui vous permet de produire une nouvelle instance d'objet héritant d'une autre d'une manière très simple:

var obj = Object.create(baseInstance);

Cette nouvelle version de la norme est mise en œuvre par tous les éditeurs de navigateurs, et je pense que nous allons commencer à voir davantage d'héritage prototypique pur ...

28
CMS

Il n'y a vraiment pas beaucoup de choix entre les deux méthodes. L'idée de base à comprendre est que, lorsque le moteur JavaScript reçoit la propriété d'un objet à lire, il vérifie d'abord l'instance et, si cette propriété est manquante, il vérifie la chaîne de prototypes. Voici un exemple qui montre la différence entre le prototype et le classique:

Prototypal

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)

Classique avec méthodes d'instance (Inefficace car chaque instance stocke sa propre propriété)

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

Efficace classique

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"

Comme vous pouvez le constater, puisqu'il est possible de manipuler le prototype de "classes" déclarées dans le style classique, l'utilisation de l'héritage prototypique ne présente aucun avantage. C'est un sous-ensemble de la méthode classique.

10
Noel Abrahams

Développement Web: héritage prototypique ou héritage classique

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

Héritage prototypique classique vs - débordement de pile

Héritage prototypique classique vs

2
ratty