Skip to content

Instantly share code, notes, and snippets.

@Cycymomo
Last active December 28, 2015 07:59
Show Gist options
  • Save Cycymomo/7468229 to your computer and use it in GitHub Desktop.
Save Cycymomo/7468229 to your computer and use it in GitHub Desktop.
explainHeritage

Comment faire de l'héritage en JavaScript ?

ou la POO classique, c'est pas automatique !

tl;dr

Cet article présente les différentes manières de faire de l'héritage en JavaScript. Il répondra aussi à ces questions :

  • Y a t il des classes en JavaScript comme dans les autres langages ?
  • Quelles sont les différentes façons de faire de l'héritage en JavaScript ?
  • Quelle est la bonne pratique ?

De la POO en JavaScript, sans classe ?

[img center] La POO classique, c'est pas automatique.

Un des plus gros problèmes, voire LE plus gros problème, de JavaScript est qu'il n'est pas appris au même titre que l'est Java ou encore C++ (pour ne citer qu'eux). Résultat ? Les développeurs ont, souvent, la notion du paradigme POO orientée classes (POO classique), et pensent, pour beaucoup d'entre eux, que c'est la seule façon de faire. C'est faux. La POO ne se limite pas qu'à ce paradigme.

JavaScript est conçu pour faire de la POO orientée prototype.

La faute aux nombreux codes qui trainent sur le net et que les développeurs non avertis prennent un malin plaisir à réutiliser. Je pense par exemple à celles-ci :

Simuler le principe de classe, à l'aide des fonctions

Oui, les deux pattern "passent par prototype". Mais il y a quand même une nuance énorme. L'un est connu comme le pattern constructor de l'héritage par prototype. L'autre est le pattern prototype de l'héritage par prototype.

Le mot clé new était très en vogue et a été amené dans JavaScript. D'ailleurs, ceci n'a pas été influencé par Java 2. L'erreur, qui apporte beaucoup de confusion, a été d'utiliser les function comme constructeur d'objet en plus de l'utilisation "classique" qui est de faire une suite de traitement et de renvoyer un résultat.

Je pense que tu confonds la propriété prototype (attachée d'office aux objets créés via new Function ou par sa forme littérale function [toto](){}), du prototype de chaque objet (instance) qui permet de lier un objet "fils" à un objet "père".

L'héritage pseudo classique

####(ou le pattern constructor de l'héritage par prototype) Egalement appelé en anglais : Delegation / Differential Inheritance, Pseudo classical Inheritance

Il emploie le mot clé new suivi de Truc() (qui est un objet Function). Cet objet contient donc par défaut la propriété prototype qui va contenir toutes les méthodes/attributs "mères" de l'instance que l'on veut créer. Truc contient également par défaut la propriété prototype.constructor qui contient les directives à réaliser à l'emploie de new. Truc a également un prototype (comprendre un père), comme tout objet. En l'occurrence, le prototype de Truc est Function

function Truc() {
  /* Ceci sera contenu dans la propriété Truc.prototype.constructor */
  this.nom = 'Eich';
  this.prenom = 'Brendan';
}

/* Ceci sera contenu dans la propriété Truc.prototype */
Truc.prototype.method1 = function () {
  console.log('Je suis la méthode 1');
};
Truc.prototype.method2 = function () {
  console.log('Je suis la méthode 2');
};

/* Tu préféreras d'ailleurs cette forme équivalente histoire de mutualiser un peu */
Truc.prototype = {
  method1: function () {
    console.log('Je suis la méthode 1');
  },
  method2: function () {
    console.log('Je suis la méthode 2');
  }
}

/* Je créée une instance truc1, créée à partir de Truc */
var truc1 = new Truc();

Si on décortique truc1 : Il contient deux propriétés par défaut, telles définies dans le constructor :

  • nom: "Eich"
  • prenom: "Brendan" Il contient un prototype, comprendre un papa, qui contient donc toutes les propiétés/méthodes définies préalablement dans la propriété prototype de Truc :
  • method1
  • method2

Là on cela est dangereux, c'est si tu oublies new. En l'oubliant, prototype.constructor ne sera pas appelé et this ne "pointera" pas sur ton instance mais sur l'objet global window.

/* A NE PAS FAIRE */
var truc1 = Truc();

Ici, tu appelles une fonction en tant que telle, et non plus un constructeur d'instance. Une fonction, par définition (sans être appelée par new), retourne toujours une valeur. Si une valeur n'est pas explicitement retournée (à l'aide de return), undefined est retournée. C'est ce qui se passe dans ce cas, undefined est retourné et stocké dans truc1. Quant à Truc(), il place nom et prenom dans this (window). C'est donc le gros danger : confondre les fonctions "classiques" des fonctions faisant office de constructeur.

Citation Crockford : I have been writing JavaScript for 8 years now, and I have never once found need to use an uber function. The super idea is fairly important in the classical pattern, but it appears to be unnecessary in the prototypal and functional patterns. I now see my early attempts to support the classical model in JavaScript as a mistake.

L'héritage par prototype

(ou le pattern prototype de l'héritage par prototype)

Comment en est-on arrivé à avoir peur des prototypes alors qu'ils sont un des piliers fondateurs du langage ?

Il utilise Object.create. Il n'y a plus de notion de new ou de this, et là est toute la différence. On dit aussi que c'est de l'héritage par délégation. Je te renvoie sur mon lien ci dessus pour des exemples. On ne manipule que des objets.

/* Prototype Animal */
var Animal = {
  init: function(nom, caracsPerso, niveau) {
    this.nom = nom || 'Inconnu'; // si nom n'est pas renseigné : 'Inconnu' par défaut
    this.niveau = niveau || 1; // si niveau n'est pas renseigné : 1 par défaut
    this.caracs = caracsPerso || this.caracDefault;
  },
  caracDefault: {attaque: 0, defense: 0},
  setAttaque: function(attaque) {
    /* code pour changer la carac */
  },
  setDefense: function(defense) {
    /* code pour changer la carac */
  },
  toString: function() {
    return('['+ this.nom + ' Animal]');
  }
};

/* Prototype Ours */
var Ours = Object.create(Animal);
Ours.toString = function() {
  return('['+ this.nom + ' Ours]');
}
Ours.caracDefault = {attaque: 0, defense: 1};

/* Prototype Loup */
var Loup = Object.create(Animal);
Loup.toString = function() {
  return('['+ this.nom + ' Loup]');
}
Loup.caracDefault = {attaque: 1, defense: 0};

/* Création des instances */
ours1 = Object.create(Ours);
ours1.init('Baloo');

loup1 = Object.create(Loup);
loup1.init('Croc blanc', {attaque: 5, defense: 5});

loup2 = Object.create(Loup);
loup2.init('Ptit loup');

console.log(ours1);
console.log(loup1);
console.log(loup2);

Avec ce pattern, il faut systématiquement appeler init pour chaque instance. Tout ce qui est dans init sera copié dans l'instance car ce sont des caractéristiques propres à chaque instance d'animal. Chaque instance pointe vers un __proto__ (Parent) (Loup ou Ours) Loup et Ours pointent eux même vers un __proto__ (Animal). Animal, enfin, pointe sur le __proto__ de base JavaScript : Object

C'est ce qu'on appelle la chaîne de prototype. Si tu demandes une propriété, JavaScript va regarder dans l'objet (ours1) s'il la trouve. S'il ne la trouve pas, il va checker dans le proto, puis le proto suivant, etc, jusqu'à la fin. S'il ne trouve rien, il renvoie undefined.

Ce qui donne, pour mon instance ours1 : ours1 > Ours > Animal > Object

Imagine que tu veuilles la propriété caracDefault. JS check dans ours1 : pas trouvé. JS check dans le proto Ours : on renvoie car c'est présent. (sinon, il aurait renvoyé celle d'Animal)

Chaine des proto

En terme de mémoire, tu auras donc un seul objet Ours, un seul objet Loup, un seul objet Animal. Et autant d'instance que tu souhaites qui pointeront vers les objets cités ci avant

Sources

à voir : http://www.developpez.net/forums/d1392956-2/webmasters-developpement-web/javascript/optimisation-poo-securite-js/#post7578190 http://www.developpez.net/forums/d1425059/webmasters-developpement-web/javascript/usage-prototypes/#post7741096

Il n'est pas possible d'hériter d'un objets natif (outre Function)

mais on peut les "étendre"

Il ne faut pas confondre "hériter d'un objet natif JavaScript" et "étendre un objet natif" JavaScript". Cet article explique la différence entre ces deux notions.

Attention, extends est un mot réservé du langage.

L'idée est bonne et largement employée (CoffeeScript comme mentionné) mais cela ne s'applique pas "aux classes innées (built-in)" du langage. C'est bien pour étendre les "classes customs" qui se basent sur le constructeur de function mais pas pour les éléments natifs (Array, Date, etc). On ne peut pas, ou presque, les étendre.

Avec ton code, on est censé appeler toutes les méthodes de la classe mère (ici Date), non ? Alors pourquoi :

Date.UTC(2012,02,30); // ok
MyDate.UTC(2012,02,30); // has no method 'UTC'

ou encore :

var d = new MyDate();
console.log(d.getHours()); // this is not a Date object.

Car, en dur dans le moteur Javascript, chaque méthode de Date va vérifier que l'objet en question est bien une instance de Date. Voici un extrait du code V8 (pour prendre un moteur en exemple) :

function DateGetHours() {
  var t = DATE_VALUE(this);
  if (NUMBER_IS_NAN(t)) return t;
  return HOUR_FROM_TIME(LocalTimeNoCheck(t));
}

DATE_VALUE est une macro : DATE_VALUE(arg) = (%_ClassOf(arg) === 'Date' ? %_ValueOf(arg) : ThrowDateTypeError());, qui check donc si l'objet passé est bien une instance de Date.

Vérification : (ça affiche la propriété interne [[Class]] qui permet de catégoriser les objets JS en fonction de leur instanciation)

Object.prototype.toString.call(new Date()); // Date
Object.prototype.toString.call(new MyDate()); // Object

Donc, si une date n'est pas instanciée en utilisant new Date(), l'objet ne sera pas une Date et ne pourra pas être manipulé comme tel. Contrairement aux autres "wrappers" JS, Date n'a pas de syntaxe littéral (new Object => {}, new Array => [], new Number => 1, new String() => "", etc)

Ceci est valable pour tous les autres objets natifs de JavaScript. (tel que Array par exemple).

Il faut faire la différence entre :

  • Sous typer un natif A : Créer un sous constructeur B du constructeur A. Les instances de B (créées avec le sous constructeur de B) seraient alors des instances de A.
  • Étendre un natif A : ça serait ajouter des méthodes à son prototype (A.prototype)

Mais on choisit souvent la deuxième option car la première est quasiment impossible (comme on vient de le voir pour Date) à cause des instances qui ont :

  • des propriétés internes gérées par le moteur Javascript notées entre double crochet : [[Class]], ou encore [[PrimitiveValue]], etc ... qui ne sont pas accessibles directement en Javascript
  • un constructeur qui ne peut pas être appelé comme on appelle une fonction (ou avec super).

Par exemple, quand une instance de Array est créée (exemple : var tableau = [1, 2, 3]), tableau est accompagné d'une méthode interne (manipulée uniquement par le moteur JS) : [[DefineOwnProperty]]. Cette méthode "écoute" en continue ce qui se passe sur notre tableau. Par exemple, quand on lui ajoute un élément (tableau.push(4);), cette méthode interne se charge d'incrémenter length. C'est transparent pour nous développeur. C'est une des raisons pourquoi il est difficile d'étendre ces types natifs de Javascript, à cause de ces fonctions et propriétés invisibles qui ne sont pas "recopiés" lors de l'appel du constructeur.

// First example: appending a chain to a prototype
function Mammal () {
this.isMammal = "yes";
}
function MammalSpecies (sMammalSpecies) {
this.species = sMammalSpecies;
}
MammalSpecies.prototype = new Mammal();
MammalSpecies.prototype.constructor = MammalSpecies;
var oCat = new MammalSpecies("Felis");
alert(oCat.isMammal); // "yes"
function Animal () {
this.breathing = "yes";
}
Object.appendChain(oCat, new Animal());
alert(oCat.breathing); // "yes"
// Second example: transforming a primitive value into an instance of its constructor and append its chain to a prototype
function Symbol () {
this.isSymbol = "yes";
}
var nPrime = 17;
alert(typeof nPrime); // "number"
var oPrime = Object.appendChain(nPrime, new Symbol());
alert(oPrime); // "17"
alert(oPrime.isSymbol); // "yes"
alert(typeof oPrime); // "object"
// Third example: appending a chain to the Function.prototype object and appending a new function to that chain
function Person (sName) {
this.identity = sName;
}
var george = Object.appendChain(new Person("George"), "alert(\"Hello guys!!\");");
alert(george.identity); // "George"
george(); // "Hello guys!!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment