JavaScript offre les capacités nécessaires à la programmation orientée objet (POO). En POO, on veut créer des objets (instances) à partir de « modèles » (classes) afin qu'ils contiennent certaines données et fonctionnalités. Dans le contexte de la POO, les propriétés qui contiennent des données s'appellent des champs, et celles qui contiennent des fonctions s'appellent des méthodes.
JavaScript n'avait pas du tout de classes avant leur ajout à la spécification du langage en 2015, mais il permettait de faire de la programmation orientée objet grâce à l'héritage basé sur les prototypes.
Et même si le mot-clé class existe aujourd'hui, JavaScript reste un langage basé sur les prototypes.
Pour comprendre ce que signifie être un langage basé sur les prototypes et comment JavaScript fonctionne réellement, on va revenir à l'époque où les classes n'existaient pas.
En JavaScript, le modèle (la classe) est mis en œuvre par une fonction ordinaire.
Quand une fonction est destinée à servir de modèle, on l'appelle une fonction constructeur, et la convention veut que son nom commence par une majuscule.
Les instances (les objets) sont créées à partir du modèle grâce au mot-clé new, que l'on utilise au moment d'appeler la fonction constructeur.
function Car() {
// ...
}
const myCar = new Car();
const yourCar = new Car();
Il est important de noter qu'en JavaScript, les instances et la fonction constructeur restent liées l'une à l'autre, même après la création des instances.
Chaque objet instance possède une propriété interne cachée appelée [[prototype]] dans la spécification du langage.
Elle contient une référence à la valeur de la clé prototype de la fonction constructeur.
Oui, tu as bien lu : une fonction JavaScript peut avoir des paires clé/valeur, car dans les coulisses, c'est aussi un objet.
Pour résumer :
prototype du constructeur (de la fonction) devient le prototype de l'instance.Souvent, on veut que tous les objets dérivés (les instances) possèdent certains champs, et qu'on leur passe des valeurs initiales au moment de leur construction.
On peut s'en charger grâce au mot-clé this.
À l'intérieur de la fonction constructeur, this représente le nouvel objet qui sera créé via new.
Quand la fonction constructeur est appelée avec new, this est renvoyé automatiquement.
Cela signifie qu'on peut ajouter des champs à la nouvelle instance en les ajoutant à this dans la fonction constructeur.
function Car(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false
Les méthodes sont ajoutées via la propriété prototype de la fonction constructeur.
À l'intérieur d'une méthode, on peut accéder aux champs de l'instance via this.
Cela fonctionne grâce à la règle générale suivante.
Quand une fonction est appelée comme méthode d'un objet, son
thisest défini sur l'objet sur lequel la méthode est appelée. 1
function Car() {
this.engineRunning = false;
// ...
}
Car.prototype.startEngine = function () {
this.engineRunning = true;
};
Car.prototype.addGas = function (litre) {
// ...
};
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
Dans l'exemple ci-dessus, myCar est un objet JavaScript ordinaire, et si on l'inspecte (par exemple dans la console du navigateur), on ne trouve pas directement dans l'objet myCar de propriété startEngine ayant une fonction pour valeur.
Alors comment le code ci-dessus peut-il bien fonctionner ?
Le secret s'appelle la chaîne de prototypes.
Quand on essaie d'accéder à une propriété (champ ou méthode) d'un objet, JavaScript vérifie d'abord si la clé correspondante se trouve directement dans l'objet lui-même.
Si ce n'est pas le cas, il continue de chercher la clé dans l'objet référencé par la propriété [[prototype]] de l'objet d'origine.
Comme on l'a mentionné plus haut, dans notre exemple, [[prototype]] pointe vers la propriété prototype de la fonction constructeur.
C'est là que JavaScript trouve la fonction startEngine, puisque c'est là que nous l'avons ajoutée.
function Car() {
// ...
}
Car.prototype.startEngine = function () {
// ...
};
Et la chaîne ne s'arrête pas là.
La propriété [[prototype]] de Car.prototype (myCar.[[prototype]].[[prototype]]) référence Object.prototype (la propriété prototype de la fonction constructeur Object).
Elle contient des méthodes générales disponibles pour tous les objets JavaScript, comme toString().
En conclusion, on peut appeler myCar.toString(), et cette méthode existera parce que JavaScript la recherche dans toute la chaîne de prototypes.
Note que la chaîne de prototypes n'est parcourue que lors de la lecture d'une valeur. Définir directement une propriété ou supprimer une propriété d'un objet instance ne concerne que cette instance précise. Cela peut surprendre quand on est habitué à un langage dont l'héritage repose sur des classes.
Aujourd'hui, JavaScript permet de définir des classes avec le mot-clé class.
Cela a été ajouté à la spécification du langage en 2015.
D'une part, cela apporte du sucre syntaxique qui rend les classes plus faciles à lire et à écrire.
La nouvelle syntaxe ressemble davantage à la façon dont on écrit les classes dans des langages comme C++ ou Java.
Les développeurs qui viennent de ces langages s'adaptent plus facilement.
D'autre part, la syntaxe de classe ouvre la voie à de nouvelles fonctionnalités du langage qui n'existent pas dans la syntaxe des prototypes.
Avec la nouvelle syntaxe, les classes se définissent avec le mot-clé class, suivi du nom de la classe et du corps de la classe entre accolades.
Le corps contient la définition de la fonction constructeur, c'est-à-dire une méthode spéciale nommée constructor.
Cette fonction fonctionne exactement comme la fonction constructeur de la syntaxe des prototypes.
Le corps de la classe contient aussi toutes les définitions de méthodes.
La syntaxe des méthodes ressemble à la notation abrégée que nous avons vue pour ajouter des fonctions comme valeurs dans un objet, voir le concept Objects.
class Car {
constructor(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
startEngine() {
this.engineRunning = true;
}
addGas(litre) {
// ...
}
}
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
Garde en tête que dans les coulisses, JavaScript reste un langage basé sur les prototypes. Tous les mécanismes que nous avons vus dans la section « Syntaxe des prototypes » ci-dessus s'appliquent toujours.
Par défaut, tous les champs d'instance sont publics en JavaScript. On peut y accéder directement et leur affecter des valeurs.
Cependant, une convention bien établie veut que les champs et les méthodes qui commencent par un tiret bas soient considérés comme privés. On ne doit jamais y accéder directement depuis l'extérieur de la classe.
Les champs privés sont parfois accompagnés de getters et de setters.
Avec les mots-clés get et set, on peut définir des fonctions qui sont exécutées quand on accède à une propriété portant le même nom que la fonction, ou quand on lui affecte une valeur.
class Car {
constructor() {
this._mileage = 0;
}
get mileage() {
return this._mileage;
}
set mileage(value) {
throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
// Just an example, usually you would not provide a setter in this case.
}
}
const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.
Exemples de this - En tant que méthode d'objet, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (consulté le 3 décembre 2021) ↩
Dans cet exercice, tu vas simuler un système informatique à base de fenêtres. Tu vas créer des fenêtres que l'on peut déplacer et redimensionner. L'image suivante est représentative des valeurs avec lesquelles tu vas travailler ci-dessous.
<--------------------- screenSize.width --------------------->
^ ┌────────────────────────────────────────────────────────────┐
| │ │
| │ position.x, _ │
| │ position.y \ │
| │ \<----- size.width -----> │
| │ ^ *──────────────────────┐ │
| │ | │ title │ │
| │ | ├──────────────────────┤ │
screenSize.height │ | │ │ │
| │ size.height │ │ │
| │ | │ contents │ │
| │ | │ │ │
| │ | │ │ │
| │ v └──────────────────────┘ │
| │ │
| │ │
v └────────────────────────────────────────────────────────────┘
📣 Pour mettre en pratique toute l'étendue de tes compétences en JavaScript, essaie de résoudre les tâches 1 et 2 avec la syntaxe des prototypes et les tâches restantes avec la syntaxe des classes.
Définis une classe (fonction constructrice) nommée Size.
Elle doit avoir deux champs width et height qui stockent les dimensions actuelles de la fenêtre.
La fonction constructrice doit accepter des valeurs initiales pour ces champs.
La largeur est fournie comme premier paramètre, la hauteur comme second.
Les valeurs par défaut de la largeur et de la hauteur doivent être 80 et 60, respectivement.
Définis en plus une méthode resize(newWidth, newHeight) qui prend une nouvelle largeur et une nouvelle hauteur comme paramètres et modifie les champs pour refléter la nouvelle taille.
const size = new Size(1080, 764);
size.width;
// => 1080
size.height;
// => 764
size.resize(1920, 1080);
size.width;
// => 1920
size.height;
// => 1080
Définis une classe (fonction constructrice) nommée Position avec deux champs, x et y, qui stockent respectivement la position horizontale et verticale actuelle du coin supérieur gauche de la fenêtre.
La fonction constructrice doit accepter des valeurs initiales pour ces champs.
La valeur de x est fournie comme premier paramètre, celle de y comme second.
La valeur par défaut doit être 0 pour les deux champs.
La position (0, 0) correspond au coin supérieur gauche de l'écran, les valeurs de x augmentant quand on se déplace vers la droite et celles de y quand on se déplace vers le bas.
Définis aussi une méthode move(newX, newY) qui prend de nouveaux paramètres x et y et modifie les propriétés pour refléter la nouvelle position.
const point = new Position();
point.x;
// => 0
point.y;
// => 0
point.move(100, 200);
point.x;
// => 100
point.y;
// => 200
Définis une classe ProgramWindow avec les champs suivants :
screenSize : contient une valeur fixe de type Size avec width à 800 et height à 600size : contient une valeur de type Size, dont la valeur initiale est celle par défaut de l'instance de Size
position : contient une valeur de type Position, dont la valeur initiale est celle par défaut de l'instance de Position
Quand la fenêtre est ouverte (créée), elle a toujours la taille et la position par défaut au départ.
const programWindow = new ProgramWindow();
programWindow.screenSize.width;
// => 800
// Similar for the other fields.
Remarque : le nom ProgramWindow est utilisé à la place de Window pour différencier la classe de la classe Window native qui existe dans les environnements de navigateur.
La classe ProgramWindow doit inclure une méthode resize.
Elle doit accepter un paramètre de type Size en entrée et tenter de redimensionner la fenêtre à la taille spécifiée.
Cependant, la nouvelle taille ne peut pas dépasser certaines limites.
x = 400, y = 300 et qu'un redimensionnement à height = 400, width = 300 est demandé, alors la fenêtre sera redimensionnée à height = 300, width = 300, car l'écran n'est pas assez grand dans la direction y pour satisfaire entièrement la demande.const programWindow = new ProgramWindow();
const newSize = new Size(600, 400);
programWindow.resize(newSize);
programWindow.size.width;
// => 600
programWindow.size.height;
// => 400
En plus de la fonctionnalité de redimensionnement, la classe ProgramWindow doit aussi inclure une méthode move.
Elle doit accepter un paramètre de type Position en entrée.
La méthode move est similaire à resize, mais elle ajuste la position de la fenêtre à la valeur demandée, plutôt que la taille.
Comme pour resize, la nouvelle position ne peut pas dépasser certaines limites.
x comme pour y.x = 250, y = 100 et qu'un déplacement à x = 600, y = 200 est demandé, alors la fenêtre sera déplacée à x = 550, y = 200, car l'écran n'est pas assez grand dans la direction x pour satisfaire entièrement la demande.const programWindow = new ProgramWindow();
const newPosition = new Position(50, 100);
programWindow.move(newPosition);
programWindow.position.x;
// => 50
programWindow.position.y;
// => 100
Implémente une fonction changeWindow qui accepte une instance de ProgramWindow en entrée et modifie la fenêtre à la taille et à la position spécifiées.
La fonction doit renvoyer l'instance de ProgramWindow qui a été passée, une fois les modifications appliquées.
La fenêtre doit avoir une largeur de 400, une hauteur de 300 et être positionnée à x = 100, y = 150.
const programWindow = new ProgramWindow();
changeWindow(programWindow);
programWindow.size.width;
// => 400
// Similar for the other fields.
Inscris-toi sur Exercism pour apprendre et maîtriser JavaScript avec 37 concepts159 exercices, et un vrai mentorat humain, le tout gratuitement.