Javascript 3
Exercice 1 — Un User à l’ancienne
function User(firstname, lastname, age) {
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
}
// Sur le prototype : une seule fonction, partagée par toutes les instances
User.prototype.getFullName = function () {
return this.firstname + " " + this.lastname;
};
User.prototype.isAdult = function () {
return this.age >= 18;
};
const a = new User("Julien", "Usson", 31);
const b = new User("Louise", "Martin", 28);
console.log(a.getFullName === b.getFullName); // true
Si les méthodes étaient déclarées dans le constructeur, cette comparaison
vaudrait false : chaque instance aurait sa propre copie.
Exercice 2 — Hériter à la main
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return "The animal makes a noise";
};
function Dog(name) {
Animal.call(this, name); // 1. le constructeur parent, sur notre `this`
}
Dog.prototype = Object.create(Animal.prototype); // 2. la chaîne de prototypes
Dog.prototype.constructor = Dog; // 3. réparer le constructeur
Dog.prototype.speak = function () {
return this.name + " barks";
};
Sans la ligne 3, new Dog("Rex").constructor.name vaut "Animal" : en
remplaçant Dog.prototype, on a perdu la propriété constructor d’origine.
C’est sans conséquence la plupart du temps, mais c’est le genre de détail que
class gère à votre place.
Exercice 3 — Le même User, en classe
class User {
constructor(firstname, lastname, age) {
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
}
getFullName() {
return `${this.firstname} ${this.lastname}`;
}
isAdult() {
return this.age >= 18;
}
}
a.getFullName === b.getFullName vaut toujours true : les méthodes d’une
classe vont sur le prototype, exactement comme en ES5. Seule l’écriture change.
Exercice 4 — Hériter avec extends
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return "The animal makes a noise";
}
}
class Dog extends Animal {
speak() {
return `${this.name} barks`;
}
}
Trois lignes de plomberie ES5 remplacées par extends. Dog n’a même pas
besoin de constructeur : sans lui, celui du parent est appelé tel quel.
Utiliser this avant super() lève une ReferenceError : l’objet n’existe
pas encore tant que le constructeur parent n’a pas tourné.
Exercice 5 — Le bon speak() au bon moment
const animals = [
new Animal("Generic"),
new Dog("Rex"),
new Dog("Max"),
new Animal("Unknown"),
];
for (const animal of animals) {
// Une seule ligne d'appel, deux comportements
console.log(animal.speak());
}
Le code appelant ne sait pas — et n’a pas besoin de savoir — s’il manipule un
Animal ou un Dog.
Exercice 6 — Des méthodes sans instance
class MathUtils {
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
}
En ES5, on les posait directement sur la fonction :
function MathUtils() {}
MathUtils.add = function (a, b) { return a + b; };
new MathUtils().add(1, 2) échoue : add appartient à la classe, pas au
prototype, donc pas aux instances.
Exercice 7 — fullName qui se lit et s’écrit
class User {
constructor(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
get fullName() {
return `${this.firstname} ${this.lastname}`;
}
set fullName(value) {
const [firstname = "", ...rest] = value.trim().split(" ");
this.firstname = firstname;
this.lastname = rest.join(" ");
}
}
Avec "Jean" tout seul, lastname devient "" — pas undefined. Et le reste
est rejoint, donc "Jean de La Fontaine" garde "de La Fontaine" comme nom.
Exercice 8 — Un mot de passe vraiment privé
class User {
#password;
constructor(firstname, lastname, age, password) {
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
this.#password = password;
}
setPassword(pwd) {
this.#password = pwd;
}
checkPassword(pwd) {
return this.#password === pwd;
}
}
user.#password depuis l’extérieur est une erreur de syntaxe — le code ne
compile même pas. Et console.log(user) ne montre pas le champ. Avec l’ancienne
convention _password, les deux fonctionnaient : ce n’était qu’un panneau
« ne pas toucher ».
⚠️ Privé ne veut pas dire sécurisé : un mot de passe en clair en mémoire reste un mot de passe en clair. On en reparle à la séance sécurité.
Exercice 9 — Traduire du vieux code
class Vehicle {
constructor(brand, speed) {
this.brand = brand;
this.speed = speed;
}
accelerate(value) {
this.speed += value;
}
describe() {
return `${this.brand} is going at ${this.speed} km/h`;
}
static isVehicle(obj) {
return obj instanceof Vehicle;
}
}
class Car extends Vehicle {
constructor(brand, speed, doors) {
super(brand, speed);
this.doors = doors;
}
describe() {
return `${this.brand} car with ${this.doors} doors going at ${this.speed} km/h`;
}
}
Vehicle.isVehicle(new Car(...)) vaut toujours true : instanceof remonte la
chaîne de prototypes, et extends la branche correctement — comme le faisait
Object.create à la main.
Exercice 10 — Mini-projet : un combat
class Player {
#hp;
constructor(name, hp, level) {
this.name = name;
this.#hp = hp;
this.level = level;
}
// Lecture seule : on peut consulter les PV, pas les écrire
get hp() {
return this.#hp;
}
attack() {
console.log(`${this.name} attacks`);
return 10;
}
takeDamage(amount) {
this.#hp = Math.max(0, this.#hp - amount);
console.log(`${this.name} takes ${amount} damage. HP: ${this.#hp}`);
}
isAlive() {
return this.#hp > 0;
}
static compare(p1, p2) {
if (p1.level > p2.level) return p1;
if (p2.level > p1.level) return p2;
return null;
}
}
class Warrior extends Player {
constructor(name, hp, level, strength) {
super(name, hp, level);
this.strength = strength;
}
attack() {
console.log(`${this.name} (Warrior) attacks with strength ${this.strength}`);
return 10 + this.strength;
}
}
const conan = new Warrior("Conan", 100, 5, 15);
const goblin = new Player("Goblin", 50, 2);
// Tant que les deux sont debout, chacun frappe à son tour
let attacker = conan;
let defender = goblin;
while (conan.isAlive() && goblin.isAlive()) {
defender.takeDamage(attacker.attack());
[attacker, defender] = [defender, attacker];
}
const loser = conan.isAlive() ? goblin : conan;
console.log(`${loser.name} is dead`);
console.log(`Winner is ${conan.isAlive() ? conan.name : goblin.name}`);
Le takeDamage du parent sert aux deux classes : seul attack() est surchargé.
Et comme #hp est privé avec un getter en lecture seule, personne ne peut
tricher en écrivant player.hp = 9999.