Programmation orientée objet en JavaScript

Le JavaScript est un des langages les plus utilisés et aussi un des plus populaires du moment. Côté client, côté serveur, il est omniprésent sur le web. Malgré cela, le JS demeure mal compris par un grand nombre de développeurs. Pourtant, à mesure que son usage s'intensifie et qu'il est le cœur d'applications de plus en plus complexes, il convient de bien appréhender son modèle objet. En route pour le royaume des objets ! 3 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Le vilain petit canard

Le JavaScript apparaît pour la première fois en 1996 dans le navigateur Netscape. Face à son succès, Microsoft, sort une implémentation similaire dans son IE3. On a déjà deux navigateurs et deux versions différentes du langage… vous connaissez la suite.

Si JavaScript est un des langages les plus utilisés, c'est donc bien grâce au web. En effet, tous les terminaux qui possèdent un navigateur sont capables d'interpréter du JS. Cette spécificité du langage le rend incontournable pour qui veut programmer pour le web.

Bon nombre de développeurs se sont ainsi mis au JS par absence d'alternative, sans prendre la peine ni le temps de vraiment comprendre le langage. Tout ceci nous a mené à des codes parfois un peu crados et des pratiques pas toujours reluisantes. À cela s'ajoutent des interpréteurs aussi variés qu'il existe de navigateurs et d'OS dans la nature, et on aboutit à un langage à la réputation sulfureuse.

Depuis quelques années, il y a eu de gros efforts de standardisation, notamment de la part de l'ECMA, l'organisme chargé de rédiger les spécifications du JS, mais aussi de la part des éditeurs de navigateurs. Les cinq principaux navigateurs que sont Firefox, Chrome, IE/Edge, Safari et Opera respectent plutôt bien la spécification dans leurs dernières versions.

Ainsi, les améliorations apportées au langage, le meilleur support des standards dans les navigateurs et l'apparition de Node.js, qui permet d'exécuter du JS sur le serveur, permettent au JavaScript d'acquérir ses lettres de noblesse en tant que langage de programmation.

II. Tout est objet

En JS tout est objet. Tous les types héritent du type Object. Ainsi, que ce soient les string, les array, les bool, presque tout est objet.

Bon, je ne vais pas vous mentir, la réalité est un tout petit peu plus complexe que cela. En JS, il y a les objets et les primitives. Une primitive est simplement une donnée qui n'est pas un objet et n'a pas de méthode.

Il en existe cinq et vous les connaissez très certainement pour la plupart : string, number, boolean, null, undefined et symbol. Vous ne le savez peut-être pas, mais il y a deux moyens de créer chacun de ces types : via son type primitif, comme vous le faites 99 % du temps, et via son constructeur. Exemple :

 
Sélectionnez
var primitif = 'je suis un string';
var stringObj = new String('je suis un string, un object string !');

Là où vous vous dites peut-être que je déconne, c'est que plus haut, j'ai défini les primitives comme n'ayant pas de méthodes. Pourtant, quand on fait cela :

 
Sélectionnez
var primitif = 'je suis un string';
console.log(primitif.toUpperCase()); // "JE SUIS UN STRING"

On invoque bien la méthode toUpperCase() et cela fonctionne, pourtant notre type est primitif… Eh oui, c'est parce que JavaScript effectue automatiquement la conversion entre la primitive et l'objet String. La chaîne est temporairement transformée en un objet String le temps du traitement, puis il est détruit. Cela s'applique bien évidemment aux autres types.

Pour bien illustrer cette différence, faisons une petite expérience :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
var primitif = 'un';
var objectString = new String('un');

console.log(typeof primitif); // "string"
console.log(typeof objectString);  // "object"

if (primitif === objectString) {
  console.log('==='); // n'affiche rien
}

if (primitif == objectString) {
  console.log('=='); // "=="
}

// les primitives sont évaluées comme du code source
var prim = '2 * 6';
console.log(eval(prim)); // renvoie le nombre 12

// les objets comme des string
var obj = new String('2 * 6');
console.log(eval(obj)); // renvoie la chaîne "6 * 6"

À l'usage il y a assez peu de différences et on a tendance à utiliser les types primitifs, car ils sont plus concis. Cependant, les différences méritent d'être connues, car elles peuvent créer des comportements inattendus., propriétés et attributs

III. Objets, propriétés et attributs

En JS, un objet contient des propriétés, jusque là, tout va bien. Cependant, on le sait moins, chaque propriété possède des attributs. Sa valeur bien entendu, mais également d'autres propriétés qui lui confèrent un comportement particulier. Observons cela.

Attribut

Type

Description

[[Value]]

N'importe quelle valeur JavaScript

La valeur obtenue lorsqu'on accède à la propriété.

[[Writable]]

Booléen

S'il vaut false, la valeur de la propriété (l'attribut [[Value]]) ne peut être changé.

[[Enumerable]]

Booléen

S'il vaut true, la propriété pourra être listée par une boucle for...in. Voir également l'article sur le caractère énumérable des propriétés.

[[Configurable]]

Booléen

S'il vaut false, la propriété ne pourra pas être supprimée et les attributs autres que [[Value]] et [[Writable]] ne pourront pas être modifiés.

J'ai honteusement copié ce tableau depuis le MDN , merci les licences Creative Commons.

On a assez peu souvent à modifier ces propriétés - c'est pour cela qu'elles sont méconnues -, mais lorsque l'on a à le faire, on utilise Object.defineProperties.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
var mini = {
  model: 'mini',
  make: 'bmw',
  hp: 120,
  color: 'blue',
  tires: '17"'
};

Object.defineProperties(mini, {
  model: {
    enumerable: false
  },
  hp: {
    writable: false
  }
});

// liste toutes les propriétés sauf "model"
for (let prop in mini) {
  console.log(prop);
}

mini.hp = 200; // on tente de modifier "hp"
console.log(mini.hp); // 120  hp n'est pas modifiable

IV. Getters et setters

Les getters et setters sont des attributs un peu spéciaux. Ils permettent d'accéder à la valeur d'une propriété ou de la définir. Bien souvent, on utilise des fonctions classiques comme getters ou setters, mais il y a bien des propriétés dédiées à cela dans les objets en JS.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
var mini = {
  model: 'mini',
  make: 'bmw',
  hp: 120,
  color: 'blue',
  tires: '17"',
  get getColor() {
    // on utilise ici la syntaxe ES6 des template literals
    return `${this.model}'s color is ${this.color}`;
  },
  set paint(newColor) {
    this.color = newColor;
  }
};

console.log(mini.getColor); // "blue"

// vous remarquez qu'on ne l'appelle pas comme une fonction !
mini.paint = 'red';
console.log(mini.getColor); // "red"

Les setters et getters possèdent eux-mêmes des attributs, deux pour être exact, il s'agit de [[Enumerable]] et de [[Configurable]], on les configure exactement de la même manière que pour les autres attributs, avec Object.defineProperties.

V. Les prototypes

Comme nombre de développeurs ne prennent pas le temps de comprendre le JS, certaines notions leur échappent. Pour quelqu'un sachant déjà programmer, il est facile d'avoir un usage basique du langage et d'arriver à ses fins sans en comprendre la vraie nature.

Le JavaScript est un langage orienté objet à prototype. Bon, qu'est-ce que c'est que cela me demanderez-vous ? Inutile que j'essaie de pondre ma propre définition, celle de Wikipedia me semble très claire. Wikipedia

Je pompe toujours sur Wikipedia, mais l'article présente deux listes qui mettent bien en exergue les différences entre les deux types d'héritages.

Objets à classes :

  • une classe définie par son code source est statique ;
  • elle représente une définition abstraite de l'objet ;
  • tout objet est instance d'une classe ;
  • l'héritage se situe au niveau des classes.

Objets à prototypes :

  • un prototype défini par son code source est mutable ;
  • il est lui-même un objet au même titre que les autres ;
  • il a donc une existence physique en mémoire ;
  • il peut être modifié, appelé ;
  • il est obligatoirement nommé ;
  • un prototype peut être vu comme un exemplaire modèle d'une famille d'objets ;
  • un objet hérite des propriétés (valeurs et méthodes) de son prototype.

Je sens que cette notion de prototype reste floue, tentons d'éclaircir vos idées. En JS, chaque objet possède un lien vers un autre objet : son prototype, lui-même possédant aussi un lien vers son prototype et ainsi de suite jusqu'à ce que le prototype ne soit plus un objet, mais null.

Ainsi, lorsque l'on souhaite accéder à une propriété d'un objet, JavaScript cherche d'abord dans l'objet lui-même, puis s'il ne trouve rien, regarde dans son prototype et ainsi de suite jusqu'au début de la chaîne. Illustrons cela par l'exemple.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
// on crée un objet littéral
var o = {a: "b", b: "c"};

// on ajoute une propriété à son prototype
o.__proto__ = {d: "e"};

// on appelle cette propriété à partir de l'objet
console.log(o.d); // "e"

// vérification des propriétés propres
console.log(o.hasOwnProperty('a')); // true
console.log(o.hasOwnProperty('d')); // false

// on affiche l'objet dans les devtools (screen ci-dessous)
console.log(o);

Image non disponible

On réalise ici que l'objet possède bien deux propriétés et qu'il va chercher la propriété « d » dans son prototype lorsqu'on lui demande d'y accéder.

La chaîne de prototype ressemble ici à ceci :{a: "b", b: "c"} --> {d: "e"} --> Object.prototype --> null

C'est bien parce qu'on a le prototype de Object dans la chaîne de prototype que l'on est en mesure d'appeler des méthodes comme hasOwnProperty que l'on n'a pas explicitement définies.

Par ailleurs, notez bien que nous utilisons dans l'exemple __proto__ comme un setter, ce qui peut notablement impacter les performances. Dans la mesure du possible, on préférera recourir à d'autres méthodes, nous verrons plus loin comment définir le prototype.

VI. Un peu de vocabulaire

Avant de rentrer dans les détails de la création d'objets et d'aborder différents patterns, clarifions un peu le vocabulaire. L'appartenance de certains mots à des langages aux paradigmes objets différents, alliée à l'usage de certains termes à tort, est responsable pour une grande part de la mauvaise compréhension du modèle objet JavaScript.

VI-A. Programmation orientée objet

Commençons par le commencement. Le terme programmation orientée objet est partagé par le JS, basé sur des objets et dont l'héritage est prototypal et par les langages OO « classiques », basés sur les classes.

Pour bien marquer la différence, entre le modèle objet du JS et celui d'autres langages comme le Java, nous pourrions tenter de redéfinir les termes comme le fait Kyle Simpson dans sa série sur l'OOJS [en].

Ainsi, le JavaScript mériterait le terme d'orienté objet, car le langage est basé sur les objets dans sa forme la plus pure. Tandis que les langages objet dit « classiques », sont plutôt basés sur les classes - dans la mesure où il n'est pas possible de créer d'objet sans passer par une classe - c'est donc de la programmation orientée classe.

VI-B. Héritage

L'autre notion centrale est celle de l'héritage. Alors qu'en POO classique, lorsqu'on instancie une classe, l'objet ainsi créé contient toutes les propriétés et méthodes de cette classe et éventuellement de ses classes parentes, il en va tout autrement en JS.

En POOP le concept est plus limpide si l'on parle de délégation plutôt que d'héritage (ce concept est expliqué en profondeur dans un autre article [en] de la série de K. Simpson). L'objet n'hérite pas des propriétés d'autres objets - nous n'avons pas vraiment de classes en JS, nous y reviendrons - dans le sens où il n'en contient pas une copie, mais un lien vers un autre objet : son prototype.

L'idée de délégation prend tout son sens lorsque l'on comprend que notre objet délègue au prototype la responsabilité de trouver une propriété ou une méthode, s'il ne la possède pas lui-même.

VI-C. Méthode

On mentionne souvent le terme méthode, mais JS ne possède pas de méthode au sens classique de l'OO. Une méthode en OOJS est simplement une fonction rattachée à un objet en tant que propriété. Elle obéit aux mêmes règles que toute autre propriété, elle « hérite » de la même manière, la seule différence est qu'elle peut être appelée.

Il est bon de noter que lorsqu'une fonction contenue dans le prototype est exécutée, this fait référence à l'objet depuis lequel on appelle la fonction et non au prototype.

VI-D. Classe

Le JS n'a pas de classes à proprement parler, il n'y a pas d'implémentation de classes dans le langage. Tout est objet et l'héritage est intégralement basé sur les prototypes. ES6 introduit quelques mots clefs supplémentaires pour faciliter le travail en OO, notamment le mot clef class, mais celui-ci n'est qu'un sucre syntaxique et le fonctionnement interne reste inchangé.

VI-E. Instance

Si le JS n'a pas de classe, on est en droit de se demander s'il a des instances. En POO classique, une instance est un objet issu d'une classe. Comme souvent en JS, on utilisera les mots habituellement utilisés en POO, on parlera donc d'instance. Cependant, une instance n'est rien d'autre qu'un objet qui hérite du prototype de son constructeur.

VII. Objets globaux

JavaScript possède un certain nombre d'objets natifs. C'est le cas de l'objet String dont nous avons parlé plus haut, mais aussi de Object, Math, etc. Vous trouverez la liste exhaustive des objets globaux sur cette page du MDN.

C'est bien grâce à ces objets prédéfinis que nous pouvons invoquer des méthodes sans avoir à les définir préalablement. Elles sont définies dans le prototype des objets que nous créons.

Observons la chaîne de prototype des objets les plus communs.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
var texte = 'string ta mère !';
// texte --> String.prototype --> Object.prototype --> null

var num = 42;
// num --> Number.prototype --> Object.prototype --> null

var objet = {test: 1};
// objet --> Object.prototype --> null

var array = ['test'];
// array --> Array.prototype --> Object.prototype --> null

function fn() {
  return 'osef';
}
// fn --> Function.prototype --> Object.prototype --> null

On comprend aisément que les objets natifs que nous créons héritent de propriétés propres de leurs objets parents, puis de Objet. C'est donc pour cela qu'il est possible d'appeler toUpperCase() sur un string, mais pas sur une fonction ou sur un nombre. Cette méthode fait partie du prototype de String et non d'Object.

VIII. Créer des objets

Passons aux choses sérieuses ! Nous savons déjà créer des objets avec la syntaxe littérale, nous avons vu également qu'il est facile d'instancier des objets natifs avec le mot clef new. Voyons donc comment créer nos propres objets.

Il existe trois manières de créer des objets en JS. Comme nous l'avons vu, le langage ne possède pas de classe, de ce fait, lorsque nous parlons de constructeur, il s'agit de fonctions (elles-mêmes des objets) qui agissent comme des constructeurs.

Les deux premières méthodes vous sont déjà familières, il s'agit de la création d'objets littéraux avec {} et de l'instanciation d'objets avec le mot clef new, nous verrons par la suite comment créer nos constructeurs.

D'ailleurs, savez-vous ce qu'il se passe lorsqu'on utilise le mot clef new ? Trois choses :

  1. Un nouvel objet est créé qui hérite de Toto.prototype ;
  2. La fonction constructrice Toto est appelée avec les arguments fournis, this étant lié au nouvel objet créé. new Toto sera équivalent à new Toto() (i.e. un appel sans argument) ;
  3. L'objet renvoyé par le constructeur devient le résultat de l'expression qui contient new. Si le constructeur ne renvoie pas d'objet de façon explicite, l'objet créé à l'étape 1 sera utilisé. (En général, les constructeurs ne renvoient pas de valeur, mais si on souhaite surcharger le processus habituel, on peut utiliser cette valeur de retour.) MDN

Enfin, la troisième méthode est apparue dans la version 5 de l'ECMAscript, il s'agit de Object.create. La particularité de cette méthode est qu'elle permet de créer un nouvel objet qui « hérite » de l'objet passé en paramètre ; sans utiliser new ni employer de pattern complexe.

Évidemment, si hérite est entre guillemets, c'est bien parce qu'en réalité, la méthode create ajoute l'objet passé en paramètre au prototype du nouvel objet.

Voyons maintenant les patterns les plus communs.

VIII-A. Constructor pattern

 
Sélectionnez
function Vehicule() {
}

var voiture = new Vehicule();

Notre objet ne fait rien, mais si on l'inspecte dans les devtools, on se rend compte que son prototype est Vehicule. Plus exactement, son prototype est de type Object, cet objet possède une propriété constructor : Vehicule.

 
Sélectionnez
voiture --> Object --> Object prototype
               |
          constructor
               |--> voiture.__proto__.constructor === Vehicule

Explications. Les objets ont une propriété standard constructor qui référence Function (en JS les constructeurs sont des fonctions). Lorsqu'une fonction est déclarée, l'interpréteur crée la nouvelle fonction ainsi que son prototype.

Par la suite, lorsqu'on crée un nouvel objet à partir de notre constructeur en utilisant le mot clef new, l'objet ainsi créé contient lui aussi un prototype avec une propriété constructor. Cette dernière référence la fonction ayant servi de constructeur, elle-même contenant le __proto__ du constructeur (ici Vehicule ayant pour prototype Function).

Jusque là, l'objet créé n'a pas grand intérêt. Ajoutons-lui quelques détails :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
function Car (make, model, color, HP, tires) {
  var HP = HP;
  this.make = make;
  this.model = model;
  this.color = color;
  this.tires = tires;
  this.getHP = function () {
    return HP;
  };
}

var miniCooperS = new Car('mini', 'cooper s', 'pink', 180, '17"');

console.log(miniCooperS.color); // "pink"
console.log(miniCooperS.getHP()); // 180
console.log(miniCooperS.HP); // undefined

On a le feeling d'une POO assez classique, on instancie notre objet en lui passant les paramètres et on peut appeler ses méthodes et accéder ou modifier ses propriétés. Vous remarquerez que les propriétés définies par var sont privées et ne peuvent être accédées de l'extérieur autrement que via la fonction getter.

L'inconvénient du contructor pattern que nous venons de mettre en place est que chaque instance porte l'ensemble des propriétés et non une référence à celles-ci via le prototype. Pour vous en convaincre, il suffit de faire un petit console.log(miniCooperS) dans les devtools.

Image non disponible


Les méthodes et propriétés se trouvent dupliquées dans toutes les instances.

De ce fait, si on crée beaucoup d'instances, l'empreinte mémoire peut rapidement devenir imposante et poser des problèmes de performances.

VIII-B. Constructor prototype pattern

On réalise qu'il est contreproductif de tout stocker dans chaque objet. Ceci nous amène tout naturellement au constructor/prototype pattern (ouais c'est super original comme nom…).

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
function Car (make, model, color, HP, tires) {
  var HP = HP;
  this.make = make;
  this.model = model;
  this.color = color;
  this.tires = tires;
  this.getHP = function () {
    return HP;
  };
}

Car.prototype = {
  utility: 'transport',
  field: 'ground',
  changeTires: function (size) {
    this.tires = size;
  } 
};

var miniCooperS = new Car('mini', 'cooper s', 'pink', 180, '17"');

console.log(miniCooperS.tires); // 17"
miniCooperS.changeTires('16"');
console.log(miniCooperS.tires); // 16"

Vous noterez que getHP() doit rester dans le constructeur, sinon, faute de closure, la fonction ne pourra plus accéder à l'attribut privé.

Image non disponible

Nos méthodes et propriétés communes se trouvent toutes dans le prototype.

Vous comprenez ici immédiatement l'intérêt de ce modèle. On définit les propriétés propres à chaque objet via le constructeur dès sa création tandis que les méthodes communes sont partagées via le prototype et n'encombrent pas l'espace mémoire.

VIII-C. Dynamic Prototype Pattern

Le fait de devoir définir le prototype en dehors de la fonction constructeur peut parfois être embêtant et/ou déroutant, car visuellement nous n'avons pas toute la logique encapsulée dans le constructeur.

Le dynamic prototype pattern permet d'outrepasser ce désagrément en définissant le prototype directement depuis le constructeur.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
function Car (make, model, color, HP, tires) {
  var HP = HP;
  this.make = make;
  this.model = model;
  this.color = color;
  this.tires = tires;
  this.getHP = function () {
    return HP;
  };
  
  // si changeTires n'existe pas,
  // c'est que l'objet n'a pas encore été instancié
  if (typeof this.changeTires !== "function") {
    // on peut vérifier si l'appel est exécuté plusieurs fois
    // console.log('yo yo yo');

    Car.prototype.changeTires = function (size) {
      this.tires = size;
    };
  }
}

var miniCooperS = new Car('mini', 'cooper s', 'pink', 180, '17"');
var miniCooperSWinterTires = new Car('mini', 'cooper s', 'pink', 180, '15"');

console.log(miniCooperS.tires); // 17"
miniCooperS.changeTires('16"');
console.log(miniCooperS.tires); // 16"

Il n'y a dans ce pattern pas d'avantage fonctionnel par rapport au précédent, il s'agit ici uniquement d'esthétique. Aussi libre à chacun d'opter pour l'un ou l'autre. Les plus perfectionnistes observeront que la condition sera exécutée à chaque instanciation, ce qui rajoute un petit traitement lors de la création d'objets…

VIII-D. Le problème du constructeur

Nous l'avons vu plus haut, lorsque l'on utilise new, l'objet créé contient dans son prototype une propriété constructor faisant référence à la fonction de laquelle il est issu. Lorsque l'on redéfinit le constructeur comme nous l'avons fait dans les deux exemples précédents, ce dernier est effacé et fait du coup référence à Object alors qu'il devrait faire référence à Car.

 
Sélectionnez
console.log(miniCooperS.contructor ===, Car); // false
console.log(miniCooperS.contructor === Object); // true

Bien que ça n'ait pas d'impact la plupart du temps puisque l'on n'en fait pas un usage extensif, certains codes et certaines bibliothèques s'y réfèrent, il peut donc être préférable de le conserver. Deux solutions pour cela :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
function Car () {}

// première méthode, redéfinir les propriétés une à une
// cela n'écrase pas le prototype… mais c'est un peu fastidieux
Car.prototype.make = 'mini';
Car.prototype.model = 'cooper'; 

// seconde méthode, redéfinir manuellement le constructeur
Car.prototype = {
  constructor: Car,
  make: 'mini',
  model: 'cooper',
  color: 'pink',
  tires: '17"'
}

var miniCooperS = new Car();

console.log(miniCooperS.contructor ===, Car); // true
console.log(miniCooperS.contructor === Object); // false

VIII-E. La controverse du new

De nombreux auteurs, notamment Douglas Crockford dans son livre, JavaScript: The Good Parts mettent en garde contre l'utilisation du mot clef « new » pour instancier les objets, et donc du pattern pseudoclassique (le constructor pattern).

Les arguments sont nombreux, Crockford explique que le JS est un langage expressif et que ce pattern ne sert qu'à « mimiquer » d'autres langages plus classiques tels que le Java, alors que le JS, à travers son modèle prototypal, possède de nombreuses autres manières de réutiliser le code et de fournir de l'héritage.

Plus flagrant et concret, si l'on tente d'instancier un objet en omettant le mot new, alors this ne sera pas lié à l'objet créé, mais à l'objet global (en général window dans un navigateur).

Il est peut-être quelque peu extrême de vouloir bannir new de nos codes, d'autant plus que la méthode Object.create l'utilise en interne. Néanmoins, pour éviter les erreurs, il est possible de minimiser son usage lorsque cela est possible. Notamment en utilisant des factory (ou approche fonctionnelle).

VIII-F. Factory pattern

Le factory pattern a l'avantage de découpler la logique du constructeur de son invocation (on ne se soucie pas de savoir comment ça marche quand on en a besoin) et de permettre le polymorphisme, c'est-à-dire d'avoir un objet constructeur qui permet de créer différentes choses selon le contexte ou les arguments passés. Le comportement est déterminé à l'exécution (dynamic binding).

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
// Cas le plus simple: tout dans le constructeur
function createCar (make, model, color, HP) {
  return {
    make: make,
    model: model,
    color: color,
    HP: HP,
    getHP: function () {
      return this.HP;
    }
  };
}

// si on veut utiliser l'héritage prototypal
var carProto = {
  getColor: function () {
    return this.color;
  }
};

function createCar (make, model, color, HP) {
  var obj = Object.create(carProto);
  obj.make = make;
  obj.model = model;
  obj.color = color;
  obj.HP = HP;
    
  return obj;
}

var clio = createCar('renault', 'clio', 'green', '90');

VIII-G. Prototype pattern

Le prototype pattern implémente un héritage prototypal dans lequel on crée des objets que l'on utilise comme prototypes pour d'autres objets. Il n'y a ici ni notion de classe ni de constructeur.

Ce schéma de conception permet de tirer parti de l'héritage prototypal du JS tout en s'affranchissant des carcans du modèle objet traditionnel. Il utilise pour cela Object.create.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
var, car = {
  make: 'mini',
  model: 'cooper',
  color: 'pink',
  HP: 180,
  tires: '17"',
  getHP: function () {
    return this.HP;
  }
}

var miniCooperS = Object.create(car);

Object.create accepte aussi d'autres paramètres, lesquels permettent de définir des propriétés propres à notre objet.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
var, car = {
  make: 'mini',
  model: 'cooper',
  color: 'pink',
  HP: 180,
  tires: '17"',
  getColor: function () {
    return this.HP;
  }
}

var miniCooperSClassic = Object.create(car);
var miniCooperS = Object.create(car, {make: {value: 'ford'}, tires: {value: '14"'}});

console.log(miniCooperSClassic.make); // "mini"
console.log(miniCooperS.make); // "ford"

VIII-H. OLOO pattern

Toujours dans cette logique de travailler autour de l'héritage prototypal sans utiliser new, un pattern assez récent, popularisé par Kyle Simpson dans son livre You don't know JS n'utilise que de purs objets pour l'héritage.

OLOO n'est pas le cri de Jacquouille ! Cela signifie Objects Linked to Other Objects. Petite démo :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
var Car = {
  init: function (make, model, color, HP, tires) {
    this.make = make;
    this.model = model;
    this.color = color;
    this.tires = tires;
  },
  changeTires: function (size) {
    this.tires = size;
  }
};

var miniCooperS = Object.create(Car);
var miniCooperSWinterTires = Object.create(Car);

miniCooperS.init('mini', 'cooper s', 'pink', 180, '17"');
miniCooperSWinterTires.init('mini', 'cooper s', 'pink', 180, '15"');

console.log(miniCooperS.tires); // 17"
miniCooperS.changeTires('16"');
console.log(miniCooperS.tires); // 16"

console.log(miniCooperS);

Dans l'exemple précédent, qui commence à être familier, les deux objets créés héritent de Car. On vérifiera facilement l'héritage avec un petit coup de console log sur l'un des objets.

Image non disponible


Par ailleurs, au lieu d'avoir une initialisation implicite via le constructeur, on a ici une méthode dédiée : init. Cela requiert une instruction supplémentaire, car la création et l'initialisation de l'objet consistent en deux étapes distinctes, mais on échappera ainsi à toute erreur liée à new.

Cette méthode peut avantageusement être combinée à celle du prototype afin d'exposer une API propre et de ne pas avoir à manuellement appeler la fonction init à chaque fois.

VIII-I. Parasitic combination inheritance

On revient ici à la charge avec le mot clef new. La raison pour laquelle je ne vous parle que maintenant de ce pattern est qu'il est un peu plus complexe à appréhender que les autres. Sous ce nom barbare se cache un pattern particulièrement indiqué pour l'héritage multiple.

Avant de créer nos constructeurs, nous devons définir une fonction chargée d'implémenter l'héritage. Concentrez-vous bien, c'est assez court, mais intense !

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
function inheritPrototype(childObject, parentObject) {
  // on crée un nouvel objet qui possède parentObject dans son proto
  var copyOfParent = Object.create(parentObject.prototype);

  // on définit son constructeur (écrasé par Object.create)
  copyOfParent.constructor = childObject;
  
  // enfin on définit le prototype de childObject
  // avec notre copyOfParent tout beau tout frais
  childObject.prototype = copyOfParent;
}

Le plus dur est fait, nous n'avons plus qu'à créer nos fonctions et à les faire hériter les unes des autres.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
function Vehicule(field) {
  this.utility = 'transportation'; 
  this.field = field;
}

Vehicule.prototype = {
  getColor : function () {
      console.log(this.color);
  }
}

function Car(field, make, model, color) {
  // on ajuste le scope pour que le this dans Vehicule
  // fasse bien référence au this de, Car
  // et on passe le ou les arguments 
  // ils doivent être dans le même ordre dans Vehicule et, Car
  Vehicule.apply(this, arguments)
  this.wheels = 4;
  this.make = make;
  this.model = model;
  this.color = color;
  this.handbrake = true;
  this.toogleHandbrake = function () {
    return handbrake = handbrake ? false : true;
  };
}

function Plane(field, make, model, color) {
  Vehicule.apply(this, arguments)
  this.make = make;
  this.model = model;
  this.color = color;
}

inheritPrototype(Plane, Vehicule);
inheritPrototype(Car, Vehicule);

var cessna = new Plane('sky', 'Cessna', '320', 'red&white');
var mini = new Car('road', 'mini', 'cooper', 'red');
var aston = new Car('road', 'Aston Martin', 'DB9', 'grey');

// vérification
console.log(cessna);

Vous pouvez légitimement vous demander pourquoi on utilise Object.create dans inheritPrototype au lieu de simplement faire var copyOfParent = parentObject.prototype;. Au même titre, pourquoi dans Object.create utilisons-nous parentObject.prototype; au lieu de simplement passer parentObject. Eh bien faisons l'expérience les amis !

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
// les deux méthodes sont ici commentées,
// on les décommentera l'une après l'autre pour les besoins de nos tests
function inheritPrototype(childObject, parentObject) {
  // on fait fi de Object.create
  // on crée alors simplement une RÉFÉRENCE
  // vers le prototype du parent au niveau du prototype enfant
  // on a donc un lien du prototype enfant vers le parent
  // var copyOfParent = parentObject.prototype;
  
  // si on ne copie pas parentObject.prototype, mais juste parentObject
  // on casse notre chaîne de prototype, car le prototype de nos objets
  // fait référence à véhicule et non à Object
  // notre objet ne sera donc pas capable de récupérer getColor()
  // var copyOfParent = Object.create(parentObject);
 
  copyOfParent.constructor = childObject;
  childObject.prototype = copyOfParent;
}

function Vehicule(field) {
  this.utility = 'transportation'; 
  this.field = field;
}

Vehicule.prototype = {
  getColor : function () {
      console.log(this.color);
  }
}

// déclaration des classes 

inheritPrototype(Plane, Vehicule);
inheritPrototype(Car, Vehicule);

var cessna = new Plane('sky', 'Cessna', '320', 'red&white');
var mini = new Car('road', 'mini', 'cooper', 'red');
var aston = new Car('road', 'Aston Martin', 'DB9', 'grey');

// on démontre ici que le prototype est bien lié via
// référence et non copie puisque cette méthode
// apparaît à deux endroits, même sur d'autres objets
aston.__proto__.pasBien = function () {
  console.log('portnawwaaak');
}

// on ajoute une méthode au proto de véhicule
// afin de voir  elle va
Vehicule.prototype.boostEngine = function () {
  console.log('engine has been boosted');
}

// on appelle une méthode pour voir si elle est trouvée
mini.getColor();

console.log(aston);

On voit ici la chaîne de prototype normale. On a le bon constructeur et le prototype contient nos deux méthodes.

Image non disponible


Nous avons ensuite omis Object.create. De ce fait, le prototype est simplement référencé depuis le parent vers l'enfant. On voit clairement que les méthodes apparaissent deux fois, et la modification du prototype d'une instance modifie celle des autres également.

Image non disponible


Enfin, nous avons copié l'objet parent et non son prototype. On constate que le proto du parent est la fonction véhicule, et non son prototype. La chaîne prototypale est donc cassée.

Image non disponible

IX. Les classes ES6

Maintenant que nous avons bien appréhendé le modèle objet « traditionnel » du JS, abordons la nouvelle syntaxe ES6. Au cas où vous ne le sauriez pas, la norme ECMAScript 2015 enrichit le langage de nouvelles manières de faire des classes, qui nous rapprochent encore plus du modèle pseudoclassique… en apparence.

Je dis bien « en apparence », car cette nouvelle syntaxe ne constitue vraiment que du sucre syntaxique. Sous le capot, nous avons bien l'héritage prototypal que nous chérissons tant. Si certains développeurs provenant d'autres langages se sentiront comme à la maison avec cette nouvelle syntaxe, ça risque de décoiffer encore plus lorsqu'ils se rendront compte que malgré les apparences, on n'est pas chez mémé.

Quoi qu'il en soit, malgré la confusion que les mots clefs class et cie peuvent apporter, pour les développeurs qui comme vous, connaissent la vraie nature de l'OOJS, cette syntaxe peut apporter un peu de clarté et de concision dans l'écriture de notre code. Reprenons nos exemples et mettons-les à la sauce classique.

IX-A. Constructeur et prototype

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
class Car {
  constructor(make, model, color, HP, tires) {
    var HP = HP;
    this.make = make;
    this.model = model;
    this.color = color;
    this.tires = tires;

    this.getHP = function () {
      return HP;
    };
  }
}

var mini = new Car('mini', 'cooper s', 'pink', 180, '17"');

console.log(mini);

Image non disponible

Dans ce premier cas, tout est dans le constructeur et nous retrouvons tout dans chaque objet

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
class Car {
  constructor(make, model, color, HP, tires) {
    var HP = HP;
    this.make = make;
    this.model = model;
    this.color = color;
    this.tires = tires;
    
    this.getHP = function () {
      return HP;
    };
  }
  
  get getMake() {
    return this.make;
  }
  
  set setMake(newMake) {
    this.make = newMake;
    return this.make;
  }

  // ici une méthode classique (ni setter, ni getter)
  faireLeKekeAuFeuRouge() {
    return 'vrooomm vrooomm';
  }
}

var mini = new Car('mini', 'cooper s', 'pink', 180, '17"');

console.log(mini);

Image non disponible

Ici, de manière assez classique, certaines méthodes sont placées dans le prototype de façon à partager des propriétés et/ou méthodes.

IX-B. Méthode statique

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
class Car {
  constructor(make, model, color, HP, tires) {
    var HP = HP;
    this.make = make;
    this.model = model;
    this.color = color;
    this.tires = tires;
    
    this.getHP = function () {
      return HP;
    };
  }
  
  static range(totalFuel) {
    // 7L/100km
    return totalFuel * (100 / 7);
  }
  
  get getMake() {
    return this.make;
  }
  
  set setMake(newMake) {
    this.make = newMake;
    return this.make;
  }
}

var mini = new Car('mini', 'cooper s', 'pink', 180, '17"');

console.log(mini);
console.log(Car.range(50));
Image non disponible

La méthode statique ne peut s'invoquer que sur la classe, car elle n'est présente que dans constructor.

Ce code revient à faire ceci en ES5 (avec quelques méthodes en moins pour plus de concisions) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
function Car (make, model, color, HP, tires) {
  var HP = HP;
  this.make = make;
  this.model = model;
  this.color = color;
  this.tires = tires;
}

Car.range = function range (totalFuel) {
  return totalFuel * (100 / 7);
};

var mini = new Car('mini', 'cooper s', 'pink', 180, '17"');

console.log(Car.range(50));

IX-C. Extends et super

Ces deux mots clefs permettent vraiment de simplifier l'héritage. Reprenons l'exemple mis en place dans parasitic combination inheritance.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
class Vehicule {
  constructor (field) {
    this.utility = 'transportation'; 
    this.field = field;
  }
  
  getColor() {
    console.log(this.color);
  }
}

class Car extends Vehicule {
  constructor (field, make, model, color) {
    // pour surcharger une méthode, il faut utiliser super
    // et lui passer les arguments pour la méthode parente
    super(field);
    make = make;
    model = model;
    color = color;
  }

  getColor () {
    console.log(this.color);
  }  
}

var aston = new Car('road', 'Aston Martin', 'DB9', 'grey');

Je vous laisse le soin de vérifier les propriétés et le prototype de l'objet, c'est exactement le même. C'est ici flagrant, le gain en clarté n'est pas négligeable !

X. Conclusion

Il resterait encore beaucoup à dire tant la POO est un vaste sujet. Mais nous avons tout de même passé en revue les points principaux, vous devriez être très à l'aise avec les concepts de l'OOJS si vous avez bien suivi et bien compris. Vous pourrez utiliser le sucre ES6 tout en comprenant le fonctionnement implicite du moteur JS.

Vous ne vous retrouverez donc pas comme un c** face à la nature prototypale de l'OOJS, même en faisant mumuse avec des mots clefs bien rassurants comme class ou extends.

Si vous souhaitez creuser encore un peu le sujet, je vous suggère de lire JavaScript Design Patterns de Addy Osmani et la série de livres You don't know JS de Kyle Simpson. Enfin, le Mozilla Developper Network est une ressource à ne pas négliger ; c'est officieusement la documentation officielle du JS et des API HTML5.

Et vous, ça se passe comment la POO en JS ? Aussi, n'hésitez pas à me faire part de vos remarques !

XI. Note de la rédaction de Developpez.com

Nous tenons à remercier Quentin Busuttil qui nous a aimablement autorisés à publier son tutoriel : Programmation orientée objet en JavaScript. Nous remercions également Claude Leloup pour la relecture orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Licence Creative Commons
Le contenu de cet article est rédigé par Quentin Busuttil et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Pas de Modification 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.