Cours JavaScript et TypeScript · Module 3: Classes et POO
Syntaxe des classes ES6
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 manquantesDans 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. Comment définit-on correctement une classe en JavaScript ES6 ?