Apprendre Javascript pour rendre vos pages Web interactives

Auteur: Mohamed CHINY Durée necessaire pour le cours de Apprendre Javascript pour rendre vos pages Web interactives Niveau recommandé pour le cours de Apprendre Javascript pour rendre vos pages Web interactives Supports vidéo disponibles pour ce cours Exercices de renforcement disponibles pour ce cours Quiz disponible pour ce cours

Page 32: Classes et héritage

Toutes les pages

La programmation orientée objet en Javascript à base de classes

Avant de commencer, il serait utile de savoir que ce chapitre ne traitera pas les fondements du paradigme orienté objet, mais seulement montrer comment manipuler les classes en Javascript à travers les opérations comme la création, l'instanciation et l'héritage.

Si vous voulez savoir davantage de la programmation orientée objet, je vous renvoie vers cette série qui traite la POO en PHP. Bien que le langage traité soit le PHP, les concepts traités dans ce cours-là sont communs à tous les langages de programmation orientée objet.

Cours de programmation orientée objet en PHP

Les classe en Javascirpt: est-ce vraiment du sucre syntaxique?

Dans la leçon dédiée aux objets personnalisés en Javascript, nous avons vu trois méthodes, à savoir : l’objet littéral, le constructeur object et la fonction constructeur. Ces trois méthodes permettent de faire la même chose: créer des objets dotés de leurs propres attributs et méthodes. Cependant, les personnes ayant déjà programmé en orienté objet avec d’autres langages, comme C++, Java ou PHP, sont habitués à une technique commune pour créer des objets qui s’articule autour des classes. En effet, l’entité qui donne lieu aux objets du programme est belle est bien une classe.

Une classe est une structure qui décrit l’objet qui en sera l’instance. D’ailleurs on parle de l’instanciation de classe pour désigner la création de l’objet. En Javascript, il est également possible de créer ses propres classes exactement comme on le fait dans les autres langages de programmation orientés objet. Or, certains développeurs trouvent que la technique des classes en Javascript est une sorte de « sucre syntaxique » ou « sytnaxtic sugar en anglais», car les classes n’apportent pas vraiment de nouvelles fonctionnalités par rapport aux méthodes de création d’objet vues précédemment, mais elles servent juste de rendre le code plus agréable à voir et surtout, ils permettent de s’aligner avec la syntaxe utilisée dans d’autres langages de programmation orientée objet, ce qui facilite aux développeurs habitués à ce paradigme de coder en Javascript.

Bien que les classes ont l'air d'être du sucre syntaxique en Javascript, leur utilité est quand même tangible. En effet, avec les classes on peut procéder à l'une des opérations les plus fondamentale de la programmation orientée objet, il s'agit de l'héritage.

Créer et instancier une classe en Javascript

Comme pour les langages de programmation orientée objet à classe, la création d'une classe se fait à l'aide du mot-clé class suivie du nom de celle-ci, puis des accollades qui renferment les membres de la classe qui peuvent être des attributs ou des méthodes.

Créons la classe Profil qui s’aligne avec les objets que nous avons vu dans la leçon précédente:
// Création de la classe
class Profil{
   this.pseudo="skilled_user";
   this.pass="abc123";
   this.droits=["r","w"];
   changerPass(nouveau){
      this.pass=nouveau;
   }
}

// Instanciation de la classe
Utilisateur=new Profil();
On peut créer cette même classe de la manière suivante:
// Création de la classe
Profil=class{
   this.pseudo="skilled_user";
   this.pass="abc123";
   this.droits=["r","w"];
   changerPass(nouveau){
      this.pass=nouveau;
   }
}

// Instanciation de la classe
Utilisateur=new Profil();
Vous avez constaté qu'on a initialisé les attributs pseudo, pass et droits au moment même de la création de la classe. En effet, cette pratique est déconseillée car il faut que l'initialisation des attributs se fasse au moment de l'instanciation de la classe (ou création de l'objet).

Afin d'initialiser les attributs au moment de l’instanciation de la classe, nous allons créer le constructeur de celle-ci. Il s'agit d'une méthode qui est appelée automatiquement suite à l'instanciation.

En Javascript le constructeur est identifiée par le mot-clé constructeur(). En guise d'argument, on passe les valeurs que l'on souhaite affecter aux différents attributs que l'on veut initialiser.

Dotée d'un constructeur, notre classe devient comme ceci:
// Création de la classe
class Profil{
   constructor(psd,pwd,drt){
      this.pseudo=psd;
      this.pass=pwd;
      this.droits=drt;
   }
   changerPass(nouveau){
      this.pass=nouveau;
   }
}

// Instanciationd de la classe
Utilisateur=new Profil("skilled_user","abc123",["r","w"]);

Encapsulation des attributs

En Programmation Orientée Objet, l'encapsulation désigne la manière avec laquelle on souhaite gérer la visibilité des membres (attributs et méthodes) de notre classe. En général, on préfère que les méthodes soient publiques, c'est à dire qu'ils soient accessibles de partout, que ce soit depuis l'intérieur de la classe qui les englobe ou depuis l'extérieur. Quant-aux attributs, on préfère souvent qu'ils soient privées, c'est à dire qu'ils soient seulement accessibles depuis la classe où elles sont définis.

En Javascript, les attributs d'une classe sont publiques, c'est à dire qu'elles sont accessibles de partout. A titre d'exemple si on exécute ce code:
class Profil{
   constructor(psd,pwd,drt){
      this.pseudo=psd;
      this.pass=pwd;
      this.droits=drt;
   }
   changerPass(nouveau){
      this.pass=nouveau;
   }
}

Utilisateur=new Profil("skilled_user","abc123",["r","w"]);

console.log(Utilisateur.pseudo)
On aura le résultat suivant dans la console:
skilled_user
Cela veut dire que l'attribut pseudo est accessible depuis l'extérieur de la classe.
Si on veut rendre cet attribut privé, alors il suffit de le préfixer par un dièse (#) comme ceci:
class Profil{
   constructor(psd,pwd,drt){
      this.#pseudo=psd;
      this.pass=pwd;
      this.droits=drt;
   }
   changerPass(nouveau){
      this.pass=nouveau;
   }
}

Utilisateur=new Profil("skilled_user","abc123",["r","w"]);

console.log(Utilisateur.#pseudo)
Dans le console on aura:
Uncaught SyntaxError: reference to undeclared private field or method #pseudo
L'erreur indique clairement qu'on peut pas accéder à l'attribut privé #pseudo depuis cet emplacement.
Le support MDN Web Docs indique que la pratique que l'on vient de voir et qui consiste à modifier l'encapsulation des attributs est une proposition expérimentale. Cela veut dire qu'il serait mieux de ne pas s'en servir pour le moment afin de garantir l'interopérabilité de notre code sur tous les navigateurs.

Je vous renvoie vers le lien qui traite ce sujet sur MDN Web Docs

Accesseur et mutateur (getter et setter)

L'accesseur ou Getter

L'accesseur (ou getter) est une méthode qui permet d'accéder à valeur d'un attribut. En Javascript, le fonctionnement du getter est légèrement différent par rapport aux autre langages qui supportent le paradigme orienté objet. Cependant, le getter s'avère utile si les attributs sont privés, donc le fait d'y accéder directement depuis l'extérieur de la classe causera une erreur. Le getter aura pour mission de retourner la valeur de cette attribut là même si on veut y accéder depuis l'extérieur de la classe.

En Javascript, le getter est une méthode (ou fonction) que l'on peut nommer comme on veut, mais on doit la précéder par le mot-clé get. Le getter n'accepte aucun argument et, souvent, il retourne la valeur d'un attribut.

Voilà un exemple:
class Profil{
   constructor(psd,pwd,drt){
      this.pseudo=psd;
      this.pass=pwd;
      this.droits=drt;
   }
   changerPass(nouveau){
      this.pass=nouveau;
   }

   // Déclaration du getter
   get getter(){
      return this.pseudo
   }
}
Le getter (nommé getter()) dans ce cas, est sensé retourner la valeur de l'attribut pseudo. Désormais, on peut appeler le getter comme s'il était un attribut, c'est à dire sans placer les parenthèses à sa fin, comme ceci:
console.log(Utilisateur.getter)
Sur la console on aura:
skilled_user
On peut créer autant de getters que l'on souhaite dans une classe. Chacun retournera la valeur d'un attribut distinct. Or, je l'ai souligné précédemment, le rôle du getter devient important si les attributs sont privés.

Le mutateur ou Setter

Comme le getter, le setter (ou mutateur) est une méthode que l'on appellera comme si elle était un attribut (sans les parenthèses). Son rôle par contre consiste à modifier la valeur d'un attribut, donc elle accepte un argument qui représente la nouvelle valeur que l'on souhaite affecter à l'attribut désigné. Le setter est une méthode que l'on peut nommer comme on le souhaite mais on doit la précéder par le mot-clé set.

Regardons ce code:
// Création de la classe Profil
class Profil{
   constructor(psd,pwd,drt){
      this.pseudo=psd;
      this.pass=pwd;
      this.droits=drt;
   }
   changerPass(nouveau){
      this.pass=nouveau;
   }

   // Déclaration du getter
   get getter(){
      return this.pseudo
   }

   // Déclaration du setter
   set setter(value){
      this.pseudo=value
   }
}

// Instanciation de la classe Profil
Utilisateur=new Profil("skilled_user","abc123",["r","w"]);

Utilisateur.setter="Other_user"; // Appel du setter
console.log(Utilisateur.getter); // Appel du getter

Pour appeler le getter, on le traite comme étant un attribut et la valeur qui lui a été affecté constitue automatiquement l'argument qui lui sera passé.

A l'issue de ce code on affichera à la console la nouvelle valeur de l'attribut pseudo en utilisant le getter. C'est le setter qu'on a appelé avant qui s'est chargé de modifier la valeur de cet attribut.

L'héritage

Réutiliser le code d'une classe

L'héritage est, sans doute, l'opération la plus célèbre est parmi les plus importantes de la programmation orientée objet. En effet, quand on crée une classe, celle-ci est sensé faire une tâche bien définie au moment de son instanciation, mais parfois on se trouve dans une situation où on a besoin d'exécuter des tâches supplémentaires qui s'ajoutent à ce que notre classe peut faire déjà. Donc, au lieux de modifier la classe ou recréer une nouvelle classe en copiant le code de la classe initiale puis en ajoutant les méthodes et attributs requis pour satisfaire la nouvelle tâche, on procède simplement à l'héritage.

L'héritage consiste à réutiliser dynamiquement le code d'une classe déjà présente sans rendre le code redondant. La classe résultante de l'héritage (aussi appelée classe fille) reprend alors tous les attributs et méthodes de la classes dont elle a hérité (appelée classe mère).

Regardons cet exemple:
// Création de la classe Profil
class Profil{
   constructor(psd,pwd,drt){
      this.pseudo=psd;
      this.pass=pwd;
      this.droits=drt;
   }
   changerPass(nouveau){
      this.pass=nouveau;
   }
}

// Création de la classe ProfilEtendu qui hérite de la classe Profil
class ProfilEtendu extends Profil{

}

// Instanciation de la classe ProfilEtendu
Utilisateur=new ProfilEtendu(
   "skilled_user",
   "abc123",
   ["r","w"]
);

Utilisateur.changerPass("azerty");
console.log(Utilisateur.pass);
Pour hériter d'une classe on utilise le mot-clé extends suivi du nom de la classe mère. Dans ce cas, la classe ProfilEtendu hérite de la classe Profil, ce qui veut dire que tous les attributs et méthodes de la classe mère ont été repris et réutilisés par la classe fille. D'ailleurs, nous avons instancié la classe ProfilEtendu qui ne contient rien, et pourtant on peut accéder à la méthode changerPass() et l'attribut pass comme si ces deux membres y étaient déclarés.

Imaginez maintenant que l'on souhaite doter notre nouvelle classe de son propre constructeur. Ce constructeur initialisera les mêmes attributs existant mais on en ajoutera un de plus, on l'appellera par exemple directory. Pour ne pas tout réécrire, nous allons mettre dans le nouveau constructeur la méthode super() qui appelle le constructeur de la classe mère. Dans ce cas, les trois premiers attributs seront initialisés avec les valeurs qui seront passées en argument de la méthode super() en invoquant le constructeur de la classe mère. Pour le quatrière argument, nous allons l'initialiser indépendamment.

Le code ressemblera à ceci:
// Création de la classe Profil
class Profil{
   constructor(psd,pwd,drt){
      this.pseudo=psd;
      this.pass=pwd;
      this.droits=drt;
   }
   changerPass(nouveau){
      this.pass=nouveau;
   }
}

// Création de la classe ProfilEtendu en héritant de la classe Profil
class ProfilEtendu extends Profil{
   constructor(psd,pwd,drt,dir){
      super(psd,pwd,drt); // Appel du constructeur de la classe mère
      this.directory=dir;
   }
}
// Instanciation de la classe ProfilEtendu
Utilisateur=new ProfilEtendu(
   "skilled_user",
   "abc123",
   ["r","w"],
   "/home"
);
console.log(Utilisateur.directory);
Sur la console on aura:
/home

Classes et héritage en vidéo