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 ?
[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 :
- Simple JavaScript Inheritance – John Resig
- A Base Class for JavaScript Inheritance – Dean Edwards
- Defining classes and inheritance – Prototype JavaScript Framework
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".
####(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
deTruc
: - 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.
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)
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
à 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