Bon exemple d'héritage basé sur un prototype de JavaScript

89

Je programme avec des langages OOP depuis plus de 10 ans mais j'apprends maintenant JavaScript et c'est la première fois que je rencontre l'héritage basé sur des prototypes. J'ai tendance à apprendre le plus rapidement en étudiant le bon code. Qu'est-ce qu'un exemple bien écrit d'application (ou bibliothèque) JavaScript qui utilise correctement l'héritage prototypique? Et pouvez-vous décrire (brièvement) comment / où l'héritage prototypique est utilisé, pour que je sache par où commencer la lecture?

Alex Reisner
la source
1
Avez-vous eu l'occasion de consulter cette bibliothèque de base? C'est vraiment sympa et assez petit. Si vous l'aimez, pensez à marquer ma réponse comme réponse. TIA, roland.
Roland Bouman
Je suppose que je suis dans le même bateau que toi. Je veux aussi en apprendre un peu sur ce langage prototypique, ne me limitant pas uniquement aux frameworks oop ou similaires, même s'ils sont excellents et tout, nous devons apprendre, non? Ce n'est pas seulement un framework qui le fait pour moi, même si je vais l'utiliser. Mais apprenez à créer de nouvelles choses dans de nouvelles langues avec de nouvelles façons de sortir des sentiers battus. J'aime ton style. Je vais essayer de m'aider et peut-être de vous aider. Dès que je trouverai quelque chose, je vous le ferai savoir.
marcelo-ferraz

Réponses:

48

Douglas Crockford a une belle page sur l' héritage prototypique JavaScript :

Il y a cinq ans, j'ai écrit Classical Inheritance en JavaScript. Il a montré que JavaScript est un langage prototypique sans classe et qu'il a une puissance d'expression suffisante pour simuler un système classique. Mon style de programmation a évolué depuis, comme le devrait tout bon programmeur. J'ai appris à embrasser pleinement le prototypalisme et me suis libéré des limites du modèle classique.

Les travaux Base.js de Dean Edward , Mootools's Class ou Simple Inheritance de John Resig sont des moyens d' hériter classique en JavaScript.

Gregory Pakosz
la source
Pourquoi pas simplement newObj = Object.create(oldObj);si vous le souhaitez sans cours? Sinon, remplacer par oldObjavec l'objet prototype de la fonction constructeur devrait fonctionner?
Cyker
76

Comme mentionné, les films de Douglas Crockford donnent une bonne explication sur le pourquoi et il couvre le comment. Mais pour le mettre en quelques lignes de JavaScript:

// Declaring our Animal object
var Animal = function () {

    this.name = 'unknown';

    this.getName = function () {
        return this.name;
    }

    return this;
};

// Declaring our Dog object
var Dog = function () {

    // A private variable here        
    var private = 42;

    // overriding the name
    this.name = "Bello";

    // Implementing ".bark()"
    this.bark = function () {
        return 'MEOW';
    }  

    return this;
};


// Dog extends animal
Dog.prototype = new Animal();

// -- Done declaring --

// Creating an instance of Dog.
var dog = new Dog();

// Proving our case
console.log(
    "Is dog an instance of Dog? ", dog instanceof Dog, "\n",
    "Is dog an instance of Animal? ", dog instanceof Animal, "\n",
    dog.bark() +"\n", // Should be: "MEOW"
    dog.getName() +"\n", // Should be: "Bello"
    dog.private +"\n" // Should be: 'undefined'
);

Le problème avec cette approche, cependant, est qu'elle recrée l'objet à chaque fois que vous en créez un. Une autre approche consiste à déclarer vos objets sur la pile de prototypes, comme ceci:

// Defining test one, prototypal
var testOne = function () {};
testOne.prototype = (function () {
    var me = {}, privateVariable = 42;
    me.someMethod = function () {
        return privateVariable;
    };

    me.publicVariable = "foo bar";
    me.anotherMethod = function () {
        return this.publicVariable;
    };

    return me;

}());


// Defining test two, function
var testTwo = function() {
    var me = {}, privateVariable = 42;
    me.someMethod = function () {
        return privateVariable;
    };

    me.publicVariable = "foo bar";
    me.anotherMethod = function () {
        return this.publicVariable;
    };

    return me;
};


// Proving that both techniques are functionally identical
var resultTestOne = new testOne(),
    resultTestTwo = new testTwo();

console.log(
    resultTestOne.someMethod(), // Should print 42
    resultTestOne.publicVariable // Should print "foo bar"
);

console.log(
    resultTestTwo.someMethod(), // Should print 42
    resultTestTwo.publicVariable // Should print "foo bar"
);



// Performance benchmark start
var stop, start, loopCount = 1000000;

// Running testOne
start = (new Date()).getTime(); 
for (var i = loopCount; i>0; i--) {
    new testOne();
}
stop = (new Date()).getTime();

console.log('Test one took: '+ Math.round(((stop/1000) - (start/1000))*1000) +' milliseconds');



// Running testTwo
start = (new Date()).getTime(); 
for (var i = loopCount; i>0; i--) {
    new testTwo();
}
stop = (new Date()).getTime();

console.log('Test two took: '+ Math.round(((stop/1000) - (start/1000))*1000) +' milliseconds');

Il y a un léger inconvénient en ce qui concerne l'introspection. Dumping testOne entraînera des informations moins utiles. La propriété privée "privateVariable" dans "testOne" est également partagée dans toutes les instances, également mentionnée utilement dans les réponses de shesek.

Dynom
la source
3
Notez que dans testOne privateVariableest simplement une variable dans la portée de l' IIFE , et qu'elle est partagée entre toutes les instances, vous ne devez donc pas y stocker de données spécifiques à l'instance. (sur testTwo, il est spécifique à l'instance, car chaque appel à testTwo () crée une nouvelle portée par instance)
shesek
J'ai voté pour parce que vous avez montré l'autre approche et pourquoi ne pas l'utiliser car elle fait des copies
Murphy316
Le problème de la recréation de l'objet à chaque fois est principalement dû aux méthodes recréées pour chaque nouvel objet. Cependant, nous pouvons atténuer le problème en définissant la méthode sur Dog.prototype. Donc, au lieu d'utiliser this.bark = function () {...}, nous pouvons faire en Dot.prototype.bark = function () {...}dehors de la Dogfonction. (Voir plus de détails dans cette réponse )
Huang Chao
26
function Shape(x, y) {
    this.x = x;
    this.y = y;
}

// 1. Explicitly call base (Shape) constructor from subclass (Circle) constructor passing this as the explicit receiver
function Circle(x, y, r) {
    Shape.call(this, x, y);
    this.r = r;
}

// 2. Use Object.create to construct the subclass prototype object to avoid calling the base constructor
Circle.prototype = Object.create(Shape.prototype);
Vlad Bezden
la source
3
Peut-être que l'ajout de ce lien avec votre réponse pourrait compléter l'image encore plus: developer.mozilla.org/en/docs/Web/JavaScript/Reference
...
14

Je jetterais un œil à YUI et à la Basebibliothèque de Dean Edward : http://dean.edwards.name/weblog/2006/03/base/

Pour YUI, vous pouvez jeter un coup d'œil sur le module lang , esp. la méthode YAHOO.lang.extend . Et puis, vous pouvez parcourir la source de certains widgets ou utilitaires et voir comment ils utilisent cette méthode.

Roland Bouman
la source
YUI 2 est obsolète depuis 2011, le lien vers langest donc semi-rompu. Quelqu'un veut-il le réparer pour YUI 3?
Acquitter
lang dans yui 3 ne semble pas avoir de méthode d'extension. mais comme la réponse a l'intention d'utiliser l'implémentation comme exemple, la version n'a pas d'importance.
eMBee
4

C'est l'exemple le plus clair que j'ai trouvé, tiré du livre Node de Mixu ( http://book.mixu.net/node/ch6.html ):

Je privilégie la composition à l'héritage:

Composition - La fonctionnalité d'un objet est constituée d'un agrégat de différentes classes en contenant des instances d'autres objets. Héritage - La fonctionnalité d'un objet est composée de sa propre fonctionnalité et de celle de ses classes parentes. Si vous devez avoir un héritage, utilisez l'ancien JS

Si vous devez implémenter l'héritage, évitez au moins d'utiliser une autre implémentation / fonction magique non standard. Voici comment vous pouvez implémenter un fac-similé raisonnable d'héritage dans ES3 pur (tant que vous suivez la règle de ne jamais définir de propriétés sur les prototypes):

function Animal(name) {
  this.name = name;
};
Animal.prototype.move = function(meters) {
  console.log(this.name+" moved "+meters+"m.");
};

function Snake() {
  Animal.apply(this, Array.prototype.slice.call(arguments));
};
Snake.prototype = new Animal();
Snake.prototype.move = function() {
  console.log("Slithering...");
  Animal.prototype.move.call(this, 5);
};

var sam = new Snake("Sammy the Python");
sam.move();

Ce n'est pas la même chose que l'héritage classique - mais c'est du Javascript standard et compréhensible et il a les fonctionnalités que les gens recherchent principalement: des constructeurs chaînables et la possibilité d'appeler des méthodes de la superclasse.

supershnee
la source
4

ES6 classetextends

ES6 classet ne extendssont que du sucre de syntaxe pour une manipulation de chaîne de prototype auparavant possible, et donc sans doute la configuration la plus canonique.

En savoir plus sur la chaîne de prototypes et la .recherche de propriétés sur: https://stackoverflow.com/a/23877420/895245

Maintenant, déconstruisons ce qui se passe:

class C {
    constructor(i) {
        this.i = i
    }
    inc() {
        return this.i + 1
    }
}

class D extends C {
    constructor(i) {
        super(i)
    }
    inc2() {
        return this.i + 2
    }
}
// Inheritance syntax works as expected.
(new C(1)).inc() === 2
(new D(1)).inc() === 2
(new D(1)).inc2() === 3
// "Classes" are just function objects.
C.constructor === Function
C.__proto__ === Function.prototype
D.constructor === Function
// D is a function "indirectly" through the chain.
D.__proto__ === C
D.__proto__.__proto__ === Function.prototype
// "extends" sets up the prototype chain so that base class
// lookups will work as expected
var d = new D(1)
d.__proto__ === D.prototype
D.prototype.__proto__ === C.prototype
// This is what `d.inc` actually does.
d.__proto__.__proto__.inc === C.prototype.inc
// Class variables
// No ES6 syntax sugar apparently:
// /programming/22528967/es6-class-variable-alternatives
C.c = 1
C.c === 1
// Because `D.__proto__ === C`.
D.c === 1
// Nothing makes this work.
d.c === undefined

Diagramme simplifié sans tous les objets prédéfinis:

      __proto__
(C)<---------------(D)         (d)
| |                |           |
| |                |           |
| |prototype       |prototype  |__proto__
| |                |           |
| |                |           |
| |                | +---------+
| |                | |
| |                | |
| |                v v
|__proto__        (D.prototype)
| |                |
| |                |
| |                |__proto__
| |                |
| |                |
| | +--------------+
| | |
| | |
| v v
| (C.prototype)--->(inc)
|
v
Function.prototype
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
la source
1

Les meilleurs exemples que j'ai vus sont dans JavaScript de Douglas Crockford : The Good Parts . Cela vaut vraiment la peine d'acheter pour vous aider à avoir une vision équilibrée de la langue.

Douglas Crockford est responsable du format JSON et travaille chez Yahoo en tant que gourou JavaScript.

Chris S
la source
7
responsable? cela ressemble presque à "coupable de" :)
Roland Bouman
@Roland Je pense que JSON est un joli format non verbeux pour stocker des données. Il ne l'a certainement pas inventé cependant, le format était là pour les paramètres de configuration dans Steam en 2002
Chris S
Chris S, je le pense aussi - De plus en plus souvent, j'aurais aimé que nous puissions tous ignorer XML comme format d'échange et passer immédiatement au JSON.
Roland Bouman
3
Pas grand chose à inventer: JSON est un sous-ensemble de la propre syntaxe littérale d'objet de JavaScript, qui est dans le langage depuis environ 1997.
Tim Down
@Time bon point - je ne savais pas que c'était là depuis le début
Chris S
0

Il existe un extrait d' héritage basé sur un prototype JavaScript avec des implémentations spécifiques à la version ECMAScript. Il choisira automatiquement laquelle utiliser entre les implémentations ES6, ES5 et ES3 en fonction de l'exécution actuelle.

Fuweichin
la source
0

Ajout d'un exemple d'héritage basé sur un prototype en Javascript.

// Animal Class
function Animal (name, energy) {
  this.name = name;
  this.energy = energy;
}

Animal.prototype.eat = function (amount) {
  console.log(this.name, "eating. Energy level: ", this.energy);
  this.energy += amount;
  console.log(this.name, "completed eating. Energy level: ", this.energy);
}

Animal.prototype.sleep = function (length) {
  console.log(this.name, "sleeping. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "completed sleeping. Energy level: ", this.energy);
}

Animal.prototype.play = function (length) {
  console.log(this.name, " playing. Energy level: ", this.energy);
  this.energy -= length;
  console.log(this.name, "completed playing. Energy level: ", this.energy);
}

// Dog Class
function Dog (name, energy, breed) {
  Animal.call(this, name, energy);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function () {
  console.log(this.name, "barking. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "done barking. Energy level: ", this.energy);
}

Dog.prototype.showBreed = function () {
  console.log(this.name,"'s breed is ", this.breed);
}

// Cat Class
function Cat (name, energy, male) {
  Animal.call(this, name, energy);
  this.male = male;
}

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

Cat.prototype.meow = function () {
  console.log(this.name, "meowing. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "done meowing. Energy level: ", this.energy);
}

Cat.prototype.showGender = function () {
  if (this.male) {
    console.log(this.name, "is male.");
  } else {
    console.log(this.name, "is female.");
  }
}

// Instances
const charlie = new Dog("Charlie", 10, "Labrador");
charlie.bark();
charlie.showBreed();

const penny = new Cat("Penny", 8, false);
penny.meow();
penny.showGender();

ES6 utilise une implémentation beaucoup plus simple de l'héritage avec l'utilisation de mots-clés constructeur et super.


la source