Cours JavaScript et TypeScript · Module 3: Classes et POO

Syntaxe des classes ES6

4 min de lecture
Dans cette leçon8

Bon retour à Jurassic Park ! En travaillant sur notre parc rempli de dinosaures, il nous faudra un moyen d'organiser notre code. Les classes ES6 sont l'outil parfait qui nous permettra de modéliser divers aspects de notre parc - des dinosaures eux-mêmes aux enclos, en passant par les systèmes de sécurité.

Que sont les classes ES6 ?

Les classes introduites en ES6 (ECMAScript 2015) offrent une syntaxe orientée objet pour JavaScript. Bien que JavaScript ait toujours été un langage basé sur les prototypes, les classes ES6 proposent une syntaxe plus propre et plus intuitive pour créer des objets et gérer l'héritage.

Dans le contexte de notre Jurassic Park, les classes nous permettent de définir des « plans » pour les différents types d'objets que nous allons créer.

Syntaxe de base d'une classe

Définissons notre première classe pour les dinosaures du parc :

1class Dinosaur {
2  // Corps de la classe
3}

Cette simple déclaration crée la classe Dinosaur. C'est comme un modèle que nous utiliserons pour créer des spécimens de dinosaures précis.

Déclarer les propriétés d'une classe

Les dinosaures de notre parc ont diverses caractéristiques - espèce, poids, régime, etc. Nous pouvons définir ces propriétés dans notre classe :

1class Dinosaur {
2  species; // Espèce du dinosaure
3  weight; // Poids en kilogrammes
4  diet; // Type de régime : 'carnivore', 'herbivore', 'omnivore'
5  enclosureId; // ID de l'enclos où il est gardé
6  isActive = true; // Indique si le dinosaure est actif (true par défaut)
7}

Bien que déclarer les propriétés de cette manière ne soit pas obligatoire en JavaScript, c'est une bonne pratique, surtout avec TypeScript, car cela aide à documenter et à typer notre classe.

Créer des instances de classe

Pour créer un dinosaure précis à partir de notre classe, on utilise le mot-clé new :

1// Création d'un nouveau T-Rex
2const rex = new Dinosaur();
3
4// Affectation des propriétés
5rex.species = 'Tyrannosaurus Rex';
6rex.weight = 8000;
7rex.diet = 'carnivore';
8rex.enclosureId = 'T-REX-01';

Ainsi, nous avons créé une instance de la classe Dinosaur et défini ses propriétés. Chaque dinosaure que nous créons aura son propre jeu de propriétés.

Les classes comme fonctions

En JavaScript, les classes sont un type particulier de fonction. En coulisses, lorsque nous déclarons une classe, JavaScript crée une nouvelle fonction constructeur.

1// Ces deux déclarations fonctionnent de façon similaire (mais pas identique) :
2
3// Avec une classe ES6
4class Dinosaur {
5  //
6}
7
8// Avec une fonction constructeur (ancienne méthode)
9function Dinosaur() {
10  //
11}

Cependant, les classes ES6 offrent une syntaxe bien plus propre et des fonctionnalités supplémentaires comme l'héritage, plus difficiles à mettre en œuvre avec de simples fonctions constructeur.

Les classes comme types en TypeScript

Si nous travaillons avec TypeScript, nous pouvons aussi utiliser les classes comme types :

1class Dinosaur {
2  species: string;
3  weight: number;
4  diet: 'carnivore' | 'herbivore' | 'omnivore';
5  enclosureId: string;
6  isActive: boolean = true;
7}
8
9// Fonction qui prend un dinosaure en paramètre
10function feedDinosaur(dino: Dinosaur) {
11  console.log(`Feeding ${dino.species} in enclosure ${dino.enclosureId}`);
12}
13
14const rex = new Dinosaur();
15rex.species = 'Tyrannosaurus Rex';
16// ... définir les autres propriétés
17
18feedDinosaur(rex); // Fonctionne !
19feedDinosaur({ species: 'Fake Dino' }); // Erreur - propriétés requises manquantes

Dans cet exemple, TypeScript garantit que seuls les objets conformes au type Dinosaur peuvent être passés à la fonction feedDinosaur.

Champs de classe privés

Dans les versions récentes de JavaScript (et en TypeScript), on peut définir des champs de classe privés à l'aide du symbole #. Les champs privés ne sont accessibles qu'à l'intérieur de la classe où ils ont été déclarés, ce qui favorise l'encapsulation :

1class Dinosaur {
2  species;
3  weight;
4  #healthStatus; // Champ privé - inaccessible depuis l'extérieur de la classe
5
6  checkHealth() {
7    // La méthode peut accéder au champ privé
8    return this.#healthStatus;
9  }
10
11  updateHealth(status) {
12    this.#healthStatus = status;
13  }
14}
15
16const raptor = new Dinosaur();
17raptor.updateHealth('Excellent');
18console.log(raptor.checkHealth()); // 'Excellent'
19
20// Erreur - impossible d'accéder directement à un champ privé
21// console.log(raptor.#healthStatus);

Les champs privés sont un excellent moyen de masquer les détails d'implémentation et d'empêcher une mauvaise utilisation de nos classes.

Résumé

Les classes ES6 sont une façon puissante d'organiser le code en JavaScript. Dans notre Jurassic Park, nous utiliserons les classes pour modéliser divers aspects du parc, des dinosaures aux systèmes de sécurité et aux attractions. À l'étape suivante, nous examinerons les constructeurs et les méthodes, qui ajouteront des comportements à nos classes.

Code de cette leçon : index.js
1// Classes ES6 dans le Parc Jurassique
2console.log("Bienvenue dans le système de gestion du Parc Jurassique !");
3console.log("On utilise les classes ES6 pour modéliser notre parc\n");
4
5// ===========================================
6// 1. Classe Dinosaur de base
7// ===========================================
8console.log("=== 1. Classe Dinosaur de base ===");
9
10class Dinosaur {
11  // Déclarations de propriétés (ES6+)
12  species;
13  weight;
14  diet;
15  enclosureId;
16  isActive = true; // Valeur par défaut
17
18  // Méthode affichant les informations
19  getInfo() {
20    return `${this.species} pèse ${this.weight}kg et est ${this.diet}`;
21  }
22
23  // Méthode de production de son
24  makeSound() {
25    return `${this.species} émet un son !`;
26  }
27
28  // Méthode d'alimentation
29  feed() {
30    if (!this.isActive) {
31      return `${this.species} dort et ne peut pas manger`;
32    }
33    return `Tu nourris ${this.species}. Le dinosaure est content !`;
34  }
35}
36
37// Création d'une instance sans constructeur
38const rex = new Dinosaur();
39rex.species = 'Tyrannosaurus Rex';
40rex.weight = 8000;
41rex.diet = 'carnivore';
42rex.enclosureId = 'ENC-001';
43
44console.log("Infos Rex :", rex.getInfo());
45console.log("Son Rex :", rex.makeSound());
46console.log("Nourrir Rex :", rex.feed());
47
48// ===========================================
49// 2. Classe avec constructeur
50// ===========================================
51console.log("\n=== 2. Classe avec constructeur ===");
52
53class DinosaurAdvanced {
54  constructor(species, weight, diet, enclosureId) {
55    this.species = species;
56    this.weight = weight;
57    this.diet = diet;
58    this.enclosureId = enclosureId;
59    this.isActive = true;
60    this.energy = 100;
61    this.lastFed = new Date();
62
63    console.log(`Nouveau dinosaure créé : ${species}`);
64  }
65
66  // Méthodes d'instance
67  getInfo() {
68    return {
69      species: this.species,
70      weight: this.weight,
71      diet: this.diet,
72      enclosureId: this.enclosureId,
73      energy: this.energy,
74      status: this.isActive ? 'Actif' : 'Endormi'
75    };
76  }
77
78  makeSound() {
79    if (!this.isActive) {
80      return `${this.species} dort... zzz`;
81    }
82
83    const sounds = {
84      'carnivore': 'ROAAAAR !',
85      'herbivore': 'Meuuuh !',
86      'omnivore': 'Grrrrr !'
87    };
88
89    return `${this.species} : ${sounds[this.diet] || 'Hmm ?'}`;
90  }
91
92  feed() {
93    if (!this.isActive) {
94      return `${this.species} dort et ne peut pas être nourri`;
95    }
96
97    this.energy = Math.min(100, this.energy + 20);
98    this.lastFed = new Date();
99
100    return `${this.species} nourri. Énergie : ${this.energy}%`;
101  }
102
103  sleep() {
104    this.isActive = false;
105    this.energy = Math.min(100, this.energy + 10);
106    return `${this.species} s'endort. Énergie : ${this.energy}%`;
107  }
108
109  wakeUp() {
110    this.isActive = true;
111    return `${this.species} se réveille !`;
112  }
113
114  exercise() {
115    if (!this.isActive) {
116      return `${this.species} dort et ne peut pas faire d'exercice`;
117    }
118
119    this.energy = Math.max(0, this.energy - 15);
120
121    if (this.energy < 20) {
122      return `${this.species} est fatigué et a besoin de repos. Énergie : ${this.energy}%`;
123    }
124
125    return `${this.species} fait de l'exercice ! Énergie : ${this.energy}%`;
126  }
127
128  // Getter et setter
129  get status() {
130    if (!this.isActive) return 'endormi';
131    if (this.energy > 80) return 'plein d\'énergie';
132    if (this.energy > 50) return 'actif';
133    if (this.energy > 20) return 'fatigué';
134    return 'épuisé';
135  }
136
137  set energyLevel(value) {
138    this.energy = Math.max(0, Math.min(100, value));
139  }
140}
141
142// Création de dinosaures avec constructeur
143const triceratops = new DinosaurAdvanced('Triceratops', 6000, 'herbivore', 'ENC-002');
144const velociraptor = new DinosaurAdvanced('Velociraptor', 15, 'carnivore', 'ENC-003');
145
146console.log("Infos Triceratops :", triceratops.getInfo());
147console.log("Son Velociraptor :", velociraptor.makeSound());
148
149// Test des méthodes
150console.log(triceratops.feed());
151console.log(triceratops.exercise());
152console.log("Statut Triceratops :", triceratops.status);
153
154// ===========================================
155// 3. Méthodes statiques
156// ===========================================
157console.log("\n=== 3. Méthodes statiques ===");
158
159class DinosaurManager {
160  static totalDinosaurs = 0;
161  static enclosures = new Map();
162
163  constructor(species, weight, diet, enclosureId) {
164    this.species = species;
165    this.weight = weight;
166    this.diet = diet;
167    this.enclosureId = enclosureId;
168    this.id = DinosaurManager.generateId();
169    this.isActive = true;
170
171    // Incrémenter le compteur
172    DinosaurManager.totalDinosaurs++;
173
174    // Ajouter à l'enclos
175    DinosaurManager.addToEnclosure(this);
176  }
177
178  // Méthode statique - génération d'ID
179  static generateId() {
180    return 'DINO-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
181  }
182
183  // Méthode statique - comparaison de dinosaures
184  static compare(dino1, dino2) {
185    if (dino1.weight > dino2.weight) {
186      return `${dino1.species} est plus lourd que ${dino2.species}`;
187    } else if (dino1.weight < dino2.weight) {
188      return `${dino2.species} est plus lourd que ${dino1.species}`;
189    } else {
190      return `${dino1.species} et ${dino2.species} ont un poids similaire`;
191    }
192  }
193
194  // Méthode statique - statistiques
195  static getStatistics() {
196    const byDiet = {};
197    const byEnclosure = {};
198
199    for (const [enclosureId, dinosaurs] of DinosaurManager.enclosures) {
200      byEnclosure[enclosureId] = dinosaurs.length;
201
202      dinosaurs.forEach(dino => {
203        byDiet[dino.diet] = (byDiet[dino.diet] || 0) + 1;
204      });
205    }
206
207    return {
208      total: DinosaurManager.totalDinosaurs,
209      byDiet,
210      byEnclosure
211    };
212  }
213
214  // Méthode statique - gestion de l'enclos
215  static addToEnclosure(dinosaur) {
216    if (!DinosaurManager.enclosures.has(dinosaur.enclosureId)) {
217      DinosaurManager.enclosures.set(dinosaur.enclosureId, []);
218    }
219    DinosaurManager.enclosures.get(dinosaur.enclosureId).push(dinosaur);
220  }
221
222  // Méthode statique - trouver les dinosaures dans un enclos
223  static getDinosaursInEnclosure(enclosureId) {
224    return DinosaurManager.enclosures.get(enclosureId) || [];
225  }
226
227  getInfo() {
228    return `${this.species} (ID: ${this.id}) - ${this.weight}kg`;
229  }
230}
231
232// Test des méthodes statiques
233const stegosaurus = new DinosaurManager('Stegosaurus', 5000, 'herbivore', 'ENC-002');
234const allosaurus = new DinosaurManager('Allosaurus', 4000, 'carnivore', 'ENC-004');
235const brachiosaurus = new DinosaurManager('Brachiosaurus', 80000, 'herbivore', 'ENC-005');
236
237console.log("ID généré :", DinosaurManager.generateId());
238console.log("Comparaison :", DinosaurManager.compare(stegosaurus, allosaurus));
239console.log("Statistiques du parc :", DinosaurManager.getStatistics());
240console.log("Dinosaures dans ENC-002 :", DinosaurManager.getDinosaursInEnclosure('ENC-002').map(d => d.species));
241
242// ===========================================
243// 4. Héritage et classes dérivées
244// ===========================================
245console.log("\n=== 4. Héritage de classes ===");
246
247// Classe de base
248class Animal {
249  constructor(species, weight) {
250    this.species = species;
251    this.weight = weight;
252    this.isAlive = true;
253  }
254
255  breathe() {
256    return `${this.species} respire`;
257  }
258
259  move() {
260    return `${this.species} se déplace`;
261  }
262}
263
264// Classe héritière
265class CarnivorousDinosaur extends Animal {
266  constructor(species, weight, huntingStyle, packSize = 1) {
267    super(species, weight); // Appel du constructeur de la classe de base
268    this.diet = 'carnivore';
269    this.huntingStyle = huntingStyle;
270    this.packSize = packSize;
271    this.lastHunt = null;
272  }
273
274  // Redéfinition de méthode de la classe de base
275  move() {
276    return `${this.species} se faufile silencieusement...`;
277  }
278
279  // Nouvelles méthodes spécifiques aux carnivores
280  hunt(prey) {
281    this.lastHunt = new Date();
282
283    if (this.packSize > 1) {
284      return `La meute de ${this.species} (${this.packSize} individus) chasse ${prey} avec la stratégie : ${this.huntingStyle}`;
285    } else {
286      return `${this.species} chasse ${prey} avec la stratégie : ${this.huntingStyle}`;
287    }
288  }
289
290  roar() {
291    const intensity = this.weight > 1000 ? 'PUISSANT' : 'fort';
292    return `${this.species} émet un rugissement ${intensity} : ROAAAAR !`;
293  }
294}
295
296// Deuxième classe héritière
297class HerbivorousDinosaur extends Animal {
298  constructor(species, weight, defenseType, herdSize = 1) {
299    super(species, weight);
300    this.diet = 'herbivore';
301    this.defenseType = defenseType;
302    this.herdSize = herdSize;
303    this.foodConsumed = 0;
304  }
305
306  move() {
307    if (this.herdSize > 1) {
308      return `Le troupeau de ${this.species} (${this.herdSize} individus) migre à la recherche de nourriture`;
309    }
310    return `${this.species} se promène tranquillement dans la forêt`;
311  }
312
313  graze() {
314    const amount = Math.floor(this.weight * 0.1); // 10% du poids par jour
315    this.foodConsumed += amount;
316    return `${this.species} mange des plantes. Consommé aujourd'hui : ${this.foodConsumed}kg`;
317  }
318
319  defend() {
320    return `${this.species} utilise son moyen de défense : ${this.defenseType} !`;
321  }
322
323  makeNoise() {
324    return `${this.species} émet un son calme de communication du troupeau`;
325  }
326}
327
328// Création des instances des classes héritières
329const tyrannosaurs = new CarnivorousDinosaur('Tyrannosaurus Rex', 8000, 'attaquant solitaire');
330const velociraptors = new CarnivorousDinosaur('Velociraptor', 15, 'attaque coordonnée en meute', 6);
331const triceratopsHerd = new HerbivorousDinosaur('Triceratops', 6000, 'cornes acérées', 12);
332const stegosaurusGroup = new HerbivorousDinosaur('Stegosaurus', 5000, 'pics sur la queue', 8);
333
334console.log("\nCarnivores :");
335console.log(tyrannosaurs.move());
336console.log(tyrannosaurs.hunt('Triceratops'));
337console.log(tyrannosaurs.roar());
338
339console.log(velociraptors.move());
340console.log(velociraptors.hunt('Hadrosaurus'));
341
342console.log("\nHerbivores :");
343console.log(triceratopsHerd.move());
344console.log(triceratopsHerd.graze());
345console.log(triceratopsHerd.defend());
346
347console.log(stegosaurusGroup.graze());
348console.log(stegosaurusGroup.makeNoise());
349
350// ===========================================
351// 5. Propriétés et méthodes privées (ES2022)
352// ===========================================
353console.log("\n=== 5. Champs et méthodes privés ===");
354
355class SecureDinosaur {
356  // Propriétés publiques
357  species;
358  weight;
359
360  // Propriétés privées (ES2022)
361  #dna;
362  #securityLevel;
363  #lastHealthCheck;
364
365  constructor(species, weight, dna, securityLevel) {
366    this.species = species;
367    this.weight = weight;
368    this.#dna = dna;
369    this.#securityLevel = securityLevel;
370    this.#lastHealthCheck = new Date();
371  }
372
373  // Méthode privée
374  #validateSecurity(requestedLevel) {
375    return requestedLevel <= this.#securityLevel;
376  }
377
378  #analyzeDNA() {
379    return `Analyse ADN : ${this.#dna.substring(0, 10)}...`;
380  }
381
382  // Méthodes publiques
383  getBasicInfo() {
384    return {
385      species: this.species,
386      weight: this.weight,
387      securityLevel: this.#securityLevel
388    };
389  }
390
391  getSecureInfo(securityClearance) {
392    if (!this.#validateSecurity(securityClearance)) {
393      return "Accès refusé - niveau de sécurité insuffisant";
394    }
395
396    return {
397      species: this.species,
398      weight: this.weight,
399      dnaAnalysis: this.#analyzeDNA(),
400      lastHealthCheck: this.#lastHealthCheck.toLocaleDateString(),
401      securityLevel: this.#securityLevel
402    };
403  }
404
405  performHealthCheck(doctorClearance) {
406    if (!this.#validateSecurity(doctorClearance)) {
407      return "Accès non autorisé aux examens de santé";
408    }
409
410    this.#lastHealthCheck = new Date();
411    return `Examen de santé de ${this.species} effectué avec succès`;
412  }
413}
414
415// Test des champs privés
416const secureTRex = new SecureDinosaur(
417  'Tyrannosaurus Rex',
418  8000,
419  'ATCGATCGATCGATCG...',
420  5
421);
422
423console.log("Infos de base :", secureTRex.getBasicInfo());
424console.log("Tentative d'accès avec habilitation 3 :", secureTRex.getSecureInfo(3));
425console.log("Accès avec habilitation 5 :", secureTRex.getSecureInfo(5));
426console.log("Examen de santé :", secureTRex.performHealthCheck(5));
427
428// Tentative d'accès direct aux champs privés (retournera undefined)
429console.log("Tentative d'accès à #dna :", secureTRex.dna); // undefined, et secureTRex.#dna hors de la classe est une SyntaxError
430
431// ===========================================
432// 6. Getters et setters
433// ===========================================
434console.log("\n=== 6. Getters et setters ===");
435
436class SmartDinosaur {
437  constructor(species, weight, age) {
438    this._species = species;
439    this._weight = weight;
440    this._age = age;
441    this._healthStatus = 'en bonne santé';
442  }
443
444  // Getter
445  get species() {
446    return this._species.toUpperCase();
447  }
448
449  get weight() {
450    return this._weight;
451  }
452
453  // Setter avec validation
454  set weight(value) {
455    if (typeof value !== 'number' || value <= 0) {
456      throw new Error('Le poids doit être un nombre positif');
457    }
458
459    const oldWeight = this._weight;
460    this._weight = value;
461
462    console.log(`Poids de ${this._species} modifié de ${oldWeight}kg à ${value}kg`);
463  }
464
465  get age() {
466    return this._age;
467  }
468
469  set age(value) {
470    if (typeof value !== 'number' || value < 0) {
471      throw new Error('L\'âge ne peut pas être négatif');
472    }
473
474    this._age = value;
475    this.#updateHealthStatus();
476  }
477
478  // Getter calculé
479  get bmi() {
480    // IMC simplifié pour les dinosaures (poids / longueur²)
481    const estimatedLength = Math.sqrt(this._weight / 100); // Longueur approximative
482    return (this._weight / (estimatedLength * estimatedLength)).toFixed(2);
483  }
484
485  get status() {
486    const ageCategory = this._age < 5 ? 'jeune' : this._age < 20 ? 'adulte' : 'âgé';
487    return `${this._species} : ${ageCategory}, ${this._healthStatus}, IMC : ${this.bmi}`;
488  }
489
490  // Méthode privée d'aide
491  #updateHealthStatus() {
492    if (this._age > 25) {
493      this._healthStatus = 'âge avancé';
494    } else if (this._age > 15) {
495      this._healthStatus = 'mature';
496    } else {
497      this._healthStatus = 'en bonne santé';
498    }
499  }
500}
501
502const smartRaptor = new SmartDinosaur('Velociraptor', 25, 8);
503
504console.log("Espèce (getter) :", smartRaptor.species);
505console.log("Statut :", smartRaptor.status);
506
507// Utilisation du setter
508smartRaptor.weight = 30;
509smartRaptor.age = 18;
510
511console.log("Nouveau statut :", smartRaptor.status);
512
513// Test de la validation du setter
514try {
515  smartRaptor.weight = -5; // Cela provoquera une erreur
516} catch (error) {
517  console.log("Erreur :", error.message);
518}
519
520console.log("\nFélicitations ! Tu maîtrises les classes ES6 dans le Parc Jurassique !");

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Comment définit-on correctement une classe en JavaScript ES6 ?

Articles utiles