Parcours
/
JavaScript
JavaScript
/
Exercices
/
Système de fenêtrage
Système de fenêtrage

Système de fenêtrage

Exercice d'apprentissage

Introduction

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.

Syntaxe des prototypes

Fonction constructeur

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.

Note

Pour résumer :

  • En JavaScript, les constructeurs sont des fonctions ordinaires.
  • Construire une nouvelle instance crée un objet lié à son constructeur par ce qu'on appelle son prototype.
  • Les fonctions sont des objets (des objets appelables) et peuvent donc avoir des propriétés.
  • La propriété prototype du constructeur (de la fonction) devient le prototype de l'instance.

Champs d'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

Méthodes d'instance

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 this est 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

La chaîne de prototypes

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.

Caution

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.

Syntaxe 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.

Déclarations de classe

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.

Champs privés, getters et setters

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.

  1. 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) ↩

Instructions

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.

1. Définis Size pour stocker les dimensions de la fenêtre

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

2. Définis Position pour stocker la position d'une fenêtre

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

3. Définis une classe ProgramWindow

Définis une classe ProgramWindow avec les champs suivants :

  • screenSize : contient une valeur fixe de type Size avec width à 800 et height à 600
  • size : 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.

4. Ajoute une méthode pour redimensionner la fenêtre

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.

  • La hauteur ou la largeur minimale autorisée est de 1. Les hauteurs ou largeurs demandées inférieures à 1 seront ramenées à 1.
  • La hauteur et la largeur maximales dépendent de la position actuelle de la fenêtre : les bords de la fenêtre ne peuvent pas dépasser les bords de l'écran. Les valeurs supérieures à ces limites seront ramenées à la plus grande taille possible. Par exemple, si la position de la fenêtre est 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

5. Ajoute une méthode pour déplacer la fenêtre

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.

  • La plus petite position est 0 pour x comme pour y.
  • La position maximale dans l'une ou l'autre direction dépend de la taille actuelle de la fenêtre. Les bords ne peuvent pas dépasser les bords de l'écran. Les valeurs supérieures à ces limites seront ramenées à la plus grande taille possible. Par exemple, si la taille de la fenêtre est à 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

6. Modifie une fenêtre de programme

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.
Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
JavaScript Exercism

Prêt à commencer Système de fenêtrage ?

Inscris-toi sur Exercism pour apprendre et maîtriser JavaScript avec 37 concepts159 exercices, et un vrai mentorat humain, le tout gratuitement.