Parcours
/
JavaScript
JavaScript
/
Programme
/
Prototypes et classes
Pr

Prototypes et classes en JavaScript

32 exercices

À propos de Prototypes et classes

JavaScript intègre les fonctionnalités nécessaires à la programmation orientée objet (POO). En POO, on cherche à 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 portent des données s'appellent des champs, et celles qui portent des fonctions s'appellent des méthodes.

Avant leur ajout à la spécification du langage en 2015, JavaScript n'avait pas de classes du tout, mais permettait déjà 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. Lorsqu'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 dérivées du modèle à l'aide du mot-clé new, au moment où l'on appelle 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 gardent une relation entre elles, 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 c'est aussi un objet en coulisses.

Depuis 2015, on peut accéder à [[prototype]] via Object.getPrototypeOf(). Avant cela, on pouvait y accéder par la clé __proto__ dans de nombreux environnements.

Ne confonds pas le prototype d'un objet ([[prototype]]) avec la propriété prototype de la fonction constructeur.

Note

En résumé :

  • Les constructeurs en JavaScript sont des fonctions ordinaires.
  • La construction d'une nouvelle instance crée un objet lié à son constructeur, relation que l'on appelle son prototype.
  • Les fonctions sont des objets (des objets appelables), et elles peuvent donc avoir des propriétés.
  • La propriété prototype de la fonction constructeur devient le prototype de l'instance.

Champs d'instance

Souvent, on veut que tous les objets dérivés (les instances) contiennent certains champs, et qu'on leur passe des valeurs initiales au moment de leur construction. Cela peut se faire grâce au mot-clé this. À l'intérieur de la fonction constructeur, this représente le nouvel objet qui sera créé via new. this est automatiquement renvoyé par la fonction constructeur lorsqu'elle est appelée avec new.

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.

Lorsqu'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

myCar dans l'exemple ci-dessus est un objet JavaScript ordinaire, et si on l'inspectait (par exemple dans la console du navigateur), on ne trouverait pas directement dans l'objet myCar de propriété startEngine dont la valeur serait une fonction. Alors comment le code ci-dessus fonctionne-t-il ?

Le secret s'appelle la chaîne de prototypes. Lorsqu'on essaie d'accéder à une propriété quelconque (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 poursuit sa recherche 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, car on l'y a 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, par exemple toString(). Le [[prototype]] de Object vaut généralement null, ce qui met fin à la chaîne de prototypes. En conclusion, tu peux appeler myCar.toString(), et cette méthode existera parce que JavaScript la cherche sur toute la chaîne de prototypes. Tu trouveras un exemple détaillé dans l'article MDN « Inheritance and the prototype chain ».

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é sur un objet instance, ou en supprimer une, ne concerne que cette instance précise. Cela peut te surprendre si tu as l'habitude d'un langage à héritage basé sur les classes.

Méthodes dynamiques (ajoute des méthodes à toutes les instances existantes)

JavaScript permet d'ajouter des méthodes à toutes les instances existantes, même après leur création.

On a vu que chaque instance conserve une référence à la propriété prototype de la fonction constructeur. Cela signifie que si on ajoute une entrée à cet objet prototype, cette nouvelle entrée (par exemple une nouvelle méthode) est immédiatement disponible pour toutes les instances créées à partir de cette fonction constructeur.

function Car() {
  this.engineRunning = false;
}

const myCar = new Car();
// Calling myCar.startEngine() here would result in "TypeError:
// myCar.startEngine is not a function".

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

myCar.startEngine();
// This works, even though myCar was created before the method
// was added.

En théorie, les méthodes dynamiques permettent même d'étendre les fonctionnalités d'objets natifs comme Object ou Array en modifiant leur prototype. C'est ce qu'on appelle le monkey patching. Comme cette modification affecte toute l'application, il vaut mieux l'éviter pour ne pas provoquer d'effets de bord inattendus. Le seul cas d'usage raisonnable est de fournir un polyfill pour une méthode manquante dans des environnements plus anciens.

Syntaxe des classes

Aujourd'hui, JavaScript permet de définir des classes avec le mot-clé class. Cette syntaxe a été ajoutée à la spécification du langage en 2015. D'un côté, elle apporte un 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, ce qui facilite l'adaptation des développeurs qui en viennent. De l'autre, la syntaxe des classes ouvre la voie à de nouvelles fonctionnalités du langage, indisponibles avec la syntaxe des prototypes.

Déclarations de classes

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 se comporte 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 est semblable à la notation abrégée que nous avons vue pour ajouter des fonctions comme valeurs dans un objet, voir le concept Objets.

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

Comme pour les déclarations et les expressions de fonction, JavaScript prend aussi en charge les expressions de classe, en plus de la déclaration de classe présentée ci-dessus.

Garde à l'esprit qu'en 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 et leur affecter une valeur directement.

L'ajout de véritables champs privés à la spécification du langage est encore en cours, voir le document de proposition pour plus de détails.

En attendant, tu peux utiliser la convention bien établie selon laquelle les champs et les méthodes qui commencent par un tiret bas doivent être 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, tu peux définir des fonctions qui sont exécutées lorsqu'on lit ou qu'on affecte une propriété portant le même nom que la fonction.

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.

Champs et méthodes de classe

En POO, on veut parfois fournir des champs ou des méthodes utilitaires qui ne dépendent pas de l'instance particulière. Ils sont alors définis pour la classe elle-même. Cela se fait avec le mot-clé static.

class Car {
  static type = 'vehicle';

  static isType(targetType) {
    return targetType === 'vehicle';
  }
}

Car.type;
// => 'vehicle'

Car.isType('road sign');
// => false

Héritage basé sur les classes

Outre le type d'héritage le long de la chaîne de prototypes que nous avons vu plus tôt, tu peux aussi représenter l'héritage entre classes en JavaScript. Ce sujet est traité dans le concept Héritage.


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

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet

Apprends Prototypes et classes