javascript-pour-les-jedis
JavaScript

JavaScript pour les Jedis, épisode III : La revanche des prototypes

Nous voila arrivés à la dernière mission vers notre quête : devenir un Jedi JavaScript confirmé ne craignant pas le côté obscure de la force. Pour rappel, durant les deux premiers épisodes de cette série d’articles consacrés à l’apprentissage du langage JavaScript dans le but de faire de vous des Jedi, nous avons abordé le côté fonctionnel du langage ainsi que la notion de fermeture (ou “closure” en anglais) qui permet de rendre ces fonctions encore plus polyvalentes et très utiles.

Dans ce dernier épisode nous allons nous attaquer à un dernier pilier du langage JavaScript que sont les prototypes. Probablement que certains d’entre vous sont déjà familiers avec cette notion de prototypes pensent qu’elle est étroitement liée à la programmation orienté objet. Mais à vrai dire, c’est encore une des nombreuses qualités des fonctions. Oui, j’ai bien dit “fonctions” !

JavaScript a emprunté les prototypes au langage Self, un langage orienté objet et dialecte de SmallTalk. Les prototypes sont utilisés pour définir des attributs et des fonctions qui seront appliqués automatiquement à un objet au moment de sa création. Une fois définit, le prototype servira de modèle, pour les objets crées. Vous l’aurez sûrement compris donc, les prototypes jouent le même rôle que les classes dans les langages à base de classes. C’est pour cela qu’en JavaScript, les prototypes sont souvent utilisés par les développeurs pour faire de la programmation orienté objet en mimant la syntaxe des “classes”.

Les objets

En JavaScript, toutes les fonctions possèdent une propriété prototype dont la valeur est un objet vide. Cette propriété n’est exploitée que lorsqu’une fonction est invoquée en tant que constructeur, avec le mot-clé new, ce que nous avons déjà détaillé lors du premier épisode. Invoquer une fonction en tant que constructeur revient donc à créer une nouvelle instance.

Tentons d’expliquer ce mécanisme d’instantiation pour mieux comprendre le rôle des prototypes.

a) Instantiation d’objet

En JavaScript, la façon la plus simple pour instancier un objet est comme ceci :

var jedi = {};

Cette instruction crée un objet vide près à l’emploi, et nous pouvons lui définir des propriétés comme ceci :

'use strict';

var jedi = {};
jedi.name = 'Luke';
jedi.level = 'padawan';

A l’instar de Java, JavaScript utilise le mot-clé new pour instancier de nouveaux objets à travers l’invocation de leurs constructeurs, mais il n’y a pas de définition de classes à proprement parlé en JavaScript. Au lieu, le mot-clé new, une fois appliqué à une fonction, déclenche la création d’un nouvel objet et à ce moment là, les prototypes entrent en jeu.Je pense que je ne vous apprend rien jusque là. Par contre, ceux venons d’un langages orienté objet (au sens strict du terme) préféreront sûrement avoir un peu plus d’encapsulation et plus de structuration ; c’est-à-dire avoir un sorte de constructeur, une fonction donc, dont le rôle est d’initialiser un objet dans un état connu. Ensuite, utiliser des méthodes pour modifier cet état, au lieu de modifier les attributs directement, ce qui risque d’introduire des erreurs et rend la maintenabilité du code un peu plus complexe. Il est donc préférable d’avoir un mécanisme pour consolider les attributs et méthodes des différents objets instanciés, à un seul endroit. JavaScript propose en effet ce genre de mécanisme, mais il est un peu différents des autres langages.

Prenons l’exemple suivant :

'use strict';

function Jedi() {}
Jedi.prototype.useForce = function () {
  console.log('I am using the force!');
};

var annikin = Jedi();
var luke = new Jedi();

console.log(annikin instanceof Jedi); //=> false
console.log(typeof annikin.useForce); //=> TypeError: annikin is undefined

console.log(luke instanceof Jedi); //=> true
console.log(typeof luke.useForce); //=> 'function'

Dans un premier temps, la fonction a été invoquée normalement et son résultat a été mémorisé dans la variable annikin. Étant donné que la fonction Jedi ne retourne rien, la variable annikin a bien la valeur undefined ; et sans surprise, annikin ne possède pas de méthode useForce. Par contre, en invoquant la fonction Jedi en tant que constructeur avec l’opérateur new, le résultat est tout autre. Il se trouve que cette fois-ci un nouvel objet a été créé et positionné comme étant le contexte d’exécution de la fonction, et le résultat retourné par le constructeur est la référence vers cet objet. L’inspection de l’instance luke nous prouve bien que cette instance de Jedi et l’objet luke possèdent bien la méthode useForce, récupérée depuis son prototype.Analysons ce simple exemple : nous avons définit une fonction Jedi, qui ne fait rien et que nous avons invoqué de deux manières : en tant que fonction et en tant que constructeur. Après la création de la fonction, nous avons ajouté une méthode useForce au prototype de cette fonction.

schema-1a

Ce premier exemple démontre que le rôle des prototypes est bien de servir de modèle aux objets instanciés. Juste le fait d’attacher la méthode useForce au prototype de Jedi l’a rendu disponible dans l’instance créée.

De manière générale, voici un schéma illustrant les liens entre une instance, son constructeur et le prototype :

schema-1c

Nous avons vu que l’opérateur new créé un nouvel objet qui sert par la suite de contexte d’exécution de la fonction (le constructeur), et nous avons également vu qu’il était possible d’attacher des propriétés au prototype de cette fonction. Puisque la fonction possède un contexte, nous pouvons également attacher des propriétés au constructeur directement via le paramètre this. Étudions ce cas de près :

'use strict';

function Jedi() {
  this.useForce = function () {
    return 'I am the Instance';
  };
}

// 1) nous attachons la méthode “useForce" au prototype
Jedi.prototype.useForce = function () {
  return 'I am the Prototype';
};

// 2) nous créons une instance
var luke = new Jedi();
console.log(luke.useForce()); //=> 'I am the Instance'

En (2), après avoir créé une instance luke, et invoqué la méthode useFroce, nous constatons que c’est bien la méthode définie dans le constructeur qui a été invoquée. L’ordre d’initialisation des propriétés est donc très important et suit la logique suivante :Comme pour l’exemple précédent, en (1) nous avons attaché une méthode useFroce au prototype du constructeur. De plus, nous avons ajouté une méthode portant le même nom au sein du constructeur. Le deux méthodes retournent un résultat différent pour que nous puissions savoir laquelle a été appelée.

  1. Les propriétés sont attachées à l’instance de l’objet  depuis le prototype ;
  2. Les propriétés sont ajoutées à l’instance de l’objet depuis le constructeur.

Autrement dit, les propriétés ajoutées dans le constructeur passent toujours avant celles attachées au prototype. La raison est que le this, autrement dit le contexte, dans le constructeur représente l’instance elle-même.

schema-1b

Étudions un autre cas afin de mieux comprendre le lien entre les prototypes et les instances d’objets. Prenons le code de l’exemple précédent et modifions le un peu pour avoir ceci :

'use strict';

function Jedi() {
  this.useForce = function () {
    return 'I am the Instance';
  };
}

// 1) nous créons une instance...
var luke = new Jedi();

// 2) ... ensuite nous attachons la méthode "useForce"
Jedi.prototype.useForce = function () {
  return 'I am the Prototype';
};

console.log(luke.useForce()); //=> 'I am the Instance'

Nous aurions pu penser que les propriétés attachées au prototype sont simplement copiées vers l’objet au moment de sa création, et qu’ensuite tout changement effectué sur le prototype après la construction de l’objet ne serait pas reporté sur l’instance créée ? En réalité les propriétés du prototype ne sont pas du tout copiées, c’est plutôt le prototype lui-même qui est attaché à l’objet construit.Dans ce code, nous avons échangé l’ordre de création de l’instance (1) et celui d’attachement de la méthode useForce au prototype (2). Pourtant, si nous invoquons cette méthode, elle est bien présente, et son résultat et celui attendu. Comment se fait-il ?

En JavaScript, chaque objet possède une propriété appelée constructor qui référence le constructeur qui a été invoqué pour créer l’objet en question. Comme le prototype est une propriété du constructeur, chaque objet sait donc comment accéder à son prototype. Vérifions cela avec cet exemple :

console.log(luke.constructor.toString());
/*
"function Jedi(){
  this.useForce = function(){
    return 'I am the Instance';
  };
}"
*/
console.log(luke.constructor.prototype.useForce.toString());
/*
"function (){
  return 'I am the Prototype';
}"
*/

Je vous laisse imaginer l’étendu de ce que nous offre ce genre de fonctionnalités. Nous pouvons faire des librairies que les utilisateurs pourront enrichir, même après que tous les objets ont été instanciés.Nous avons bien accès au constructeur de l’instance luke ainsi qu’à son prototype et donc à toutes les propriétés attachées au prototype ; Ce qui permet d’expliquer pourquoi tout changement effectué sur le prototype après la création de l’objet est automatiquement présent sur ce dernier.

Maintenant que la notion de prototype n’a plus de secret pour vous, nous allons introduire une autre notion intimement liée aux prototypes : la chaîne des prototypes.

b) La chaîne des prototypes et l’héritage

Afin d’expliquer ce qu’est la chaîne des prototypes, prenons l’exemple suivant :

'use strict';

function Force() {}
Force.prototype.useForce = function () {
  return 'I am the Force to be used.';
};

function Jedi() {}
Jedi.prototype = {
 useForce: Force.prototype.useForce
};

var luke = new Jedi();
console.log(luke.useForce()); //=> 'I am the Force to be used.'
console.log(luke instanceof Jedi); //=> true
console.log(luke instanceof Force); //=> false

En testant la présence de la méthode useFoce, ainsi que la nature du type de luke, nous réalisons que luke a la faculté maintenant d’utiliser la Force, mais il ne l’incarne pas, il n’est pas la Force ! Si nous souhaitons qu’il devienne la Force, nous devrions copier toutes les propriétés de Force vers le prototype de Jedi, une par une ! Sinon, plus simplement, il suffit que Jedi hérite les propriétés de Force. Jusque là je ne vous apprend rien. Cependant, en JavaScript, nous allons nous baser sur ce que l’on appelle la chaîne des prototypes pour bénéficier de l’héritage entre les objets.Le prototype d’une fonction étant un simple objet, il existe plusieurs façon de lui attacher des propriétés. Dans l’exemple précédent, nous avons défini une Force et un Jedi, et puisqu’un Jedi est le seul individu apte à maîtriser la Force et l’utilise pour faire du bien, nous allons faire en sorte que le Jedi hérite des attributs de Force. Nous faisons cela en copiant la méthode useForce depuis le prototype de Force vers une méthode useForce dans le prototype de Jedi.

La “bonne” façon de réaliser cette chaîne est de créer une instance d’un objet et l’utiliser en tant prototype d’un autre objet :

'use strict';

function Force() {}
Force.prototype.useForce = function () {
  return 'I am the Force to be used.';
};

function Jedi() {}
Jedi.prototype = new Force();

var luke = new Jedi();

console.log(luke.useForce()); //=> 'I am the Force to be used.'
console.log(luke instanceof Jedi); //=> true
console.log(luke instanceof Force); //=> true

schema-2Maintenant, luke incarne la Force ! Voici ce que cela donne sous forme de schéma :

A titre d’exemple, voici la “mauvaise” façon de réaliser une chaîne des prototypes…

Jedi.prototype = Force.prototype;

 

Vous allez me dire, après tout, les deux prototypes sont des objets, pourquoi ne pas faire une simple affectation ? La raison est simple : les objets sont copiés par référence en JavaScript, au moment de l’affectation, les deux prototypes référenceront le même objet en mémoire. Modifier le prototype de Jedi revient à modifier celui de Force, ce qui peut conduire à des effets indésirables :

'use strict';

function Force() {}
Jedi.prototype.toString = function () {
  return 'I am the Force';
};

function Jedi() {}
Jedi.prototype = Force.prototype;
Jedi.prototype.toString = function () {
  return 'I am a Jedi';
};

var luke = new Jedi();
var force = new Force();
console.log(luke instanceof Jedi); //=> true
console.log(luke instanceof Force); //=> true
console.log(luke + ''); //=> 'I am a Jedi'
console.log(force + ''); //=> 'I am a Jedi' <=== OH OH !!!!

Vous l’aurez compris donc, cette pratique est à proscrire !Vous allez me dire, après tout, les deux prototypes sont des objets, pourquoi ne pas faire une simple affectation ? La raison est simple : les objets sont copiés par référence en JavaScript, au moment de l’affectation, les deux prototypes référenceront le même objet en mémoire. Modifier le prototype de Jedi revient à modifier celui de Force, ce qui peut conduire à des effets indésirables :

Après avoir étudié les prototypes et exploré la flexibilité et la puissance offertes par les prototypes, ainsi que la chaîne des prototypes, passons maintenant à la suite de l’épisode et tentons de mettre en pratique cette fonctionnalité pour implémenter des “classes” (comme dans les autres langages du type C++ et Java).

Implémentation d’une “classe”

Je constate souvent que pas mal de développeurs, particulièrement ceux qui viennent des langages objet à base de classes (Java ou C++ typiquement), préfèrent avoir une sorte d’abstraction leur permettant de simplifier l’implémentation de ce qu’ils appellent des “classes”.

En JavaScript, s’il y a deux choses à retenir en POO, ce sont :

  1. La notion de “classe” n’existe pas ;
  2. Il n’y a pas d’héritage de classes, mais un héritage de prototypes — puisque les classes n’existent pas !

Mais grâce aux prototypes qui nous permettent d’enrichir le langage en le rendant beaucoup plus flexible, il est possible d’avoir :

  1. une sorte de syntaxe pour implémenter des constructeurs et des prototypes ;
  2. un mécanisme simple pour réaliser l’héritage des prototypes ;
  3. un moyen d’accéder à des méthodes surchargées par le prototype.

Je tiens à préciser tout de même que ce que nous allons voir dans la suite de l’article est simplement un sucre syntaxique offert par le langage pour permettre aux développeurs qui tiennent tant aux “classes” de pouvoir “imiter” ce fonctionnement en JavaScript.

Je vous recommande donc lorsque vous développer en JavaScript de concevoir vos applications en raisonnant en “prototypes” et non pas en “classes” afin d’exploiter au maximum la puissance du langage. D’ailleurs, ceci est vrai pour n’importe quel autre langage.

Regardons maintenant comment peut-on faire des “classes” en JavaScript…Mais avant de passer à la suite, sachez qu’il existe plusieurs façons d’écrire des “classes” en JavaScript, ce qui est normal puisque comme je l’ai précisé juste avant, JavaScript ne gère pas les “classes” et la conséquence directe de cela est que l’ont va retrouver différentes implémentations dans la littérature. Pour ma part je vais vous présenter une des implémentations, la plus simple, à mon avis.

a) “Classe” et héritage en ECMAScript 5 (version actuelle de JavaScript)

Reprenons l’exemple de code de notre Jedi :

'use strict';

function Jedi(name) {
  this.name = name;
}

Jedi.prototype.toString = function () {
  return 'I am ' + this.name;
};

var luke = new Jedi('luke');
console.log(luke.toString()); //=> 'I am luke'

Essayons maintenant de coder une abstraction qui va nous permettre de créer des objets spécialisés et bénéficier d’un héritage au passage…

'use strict';

function Klass(parent, child) {
  if (!child) {
    return parent;
  }

  //1)
  child.prototype = new parent();

  //2)
  child.prototype.constructor = child;

  //3)
  return child;
}

A noter que ceci reste une implémentation naïve, à titre d’exemple, vous ne devriez pas l’utiliser dans vos applications en production.Essayons maintenant de coder une abstraction qui va nous permettre de créer des objets spécialisés et bénéficier d’un héritage au passage…

Expliquons ce que fait ce petit bout de code :

  1. Nous réalisons un héritage de prototypes à ce niveau, comme vu précédemment ;
  2. Nous corrigeons le constructeur de “child” pour qu’il pointe sur celui de “child”, car à cause de l’étape précédente, il pointe vers celui de “parent”.
  3. Enfin, nous retournons le constructeur “child” pour qu’il puisse être instancié.

Voilà ! Vous avez maintenant une API Klass vous permettant de coder des “classes”. Voici comment nous l’utilisons :

'use strict';

var force = Klass(function Force(name) {
  this.me = name || 'Force';
  this.print = function () {
    return this.me + ' Force';
  };
});
var jedi = Klass(force, function Jedi(name) {
  this.me = name || 'Jedi';
  this.print = function () {
    return this.me + ' POWA!!';
  };
});

var yoda = new jedi('Yoda');
console.log(yoda instanceof jedi); //=> true
console.log(yoda instanceof force); //=> true
console.log(yoda.print()); //=> Yoda POWA!!

Cependant, j’ai peur que ces librairies ne soient obsolètes rapidement. La raison ? La sortie prochaine de la future version de JavaScript, ECMAScript 6, qui apporte énormément de changement et de fonctionnalités qui manquaient tant au langage. Parmi elles, on retrouve une nouvelle syntaxe pour écrire des “classes” ; mais ne vous méprenez pas, cela reste uniquement un sucre syntaxique proposé par cette nouvelle version. En interne, les prototypes règnent toujours en maître.Si vous préférez utiliser ce genre de sucre syntaxique, sachez qu’il existe une multitude de micro librairies JavaScript dédiées à cet usage, que vous allez pouvoir utiliser en production.

b) “Classe” et héritage en ECMAScript 6 (prochaine version de JavaScript)

Voici à quoi va ressembler la nouvelle syntaxe permettant d’écrire de la POO en JavaScript tout en faisant plaisir aux développeurs adeptes des “classes” :

'use strict';

class Force {
  constructor(name = 'Force'){
    this.me = name;
  }
  print(){
    return this.me + ' Force';
  }
}

class Jedi extends Force {
  constructor(name = 'Jedi'){
    super();
    this.me = name;
  }
  print(){
    return this.me + ' POWA!!'
  }
}

var yoda = new Jedi();
console.log(yoda instanceof Jedi); //=> true
console.log(yoda instanceof Force); //=> true
console.log(yoda.print()); //=> Yoda POWA!!

Conclusion

Tout simplement ! Vous pouvez voir la version transpilée en ECMAScript 5 sur cette page Web en utilisant le célèbre transpileur Babel (anciennement 6to5).

Grâce à cette série d’articles consacrés à l’apprentissage des fondamentaux du langage JavaScript, nous avons appris et surtout compris les trois piliers du langage : les objets à travers la POO, les fonctions et les closures. Nous avons donc vu quelle était l’importance pour un développeur débutant en JavaScript d’apprendre vraiment ce langage afin d’en tirer le meilleur et devenir un Jedi.

Prenez le temps d’apprendre, pratiquer et surtout comprendre ces trois piliers, et vous verrez que JavaScript n’aura plus de secrets pour vous. Ceci est d’autant plus important et essentiel puisque dans les mois à venir, le langage va connaître une évolution majeure depuis sa naissance en 1995. C’est le moment idéal donc de bien maîtriser les bases du langage car cette évolution, annoncée pour Juin 2015, va apporter tout un ensemble d’améliorations et de fonctionnalités toutes aussi puissantes et utiles les unes les autres, et que tout Jedi JavaScript va devoir adopter.

Que la force d’ECMAScript soit avec vous, fidèles Jedi.

Standard