Apprenez en lisant dans l'ordre

Les bases des classes — définir une classe et créer des instances

Comment une classe permet de créer beaucoup d'objets de même forme : créer des instances avec constructor et new, écrire des méthodes qui utilisent this, et savoir quand un littéral objet suffit.

Membres, produits, coupons : dans une application, les données qui ont les mêmes champs et servent aux mêmes calculs reviennent sans cesse. Si tu écris chacune sous forme de littéral objet, tu recopies les champs et la formule de calcul pour chaque élément.

Cet article présente les classes (class), qui réunissent en un seul endroit la forme des données et le code qui les traite, et montre comment créer les objets un par un avec new.

Définir la forme des données en un seul endroit — class et new

Prenons deux types de coupons de réduction. Si tu écris { code: "WELCOME", discount: 500 } pour chaque coupon et que tu tapes discont par erreur dans un seul d'entre eux, aucune erreur n'est levée : seul le montant de réduction de ce coupon vaut undefined. Comme les noms de clés sont réécrits dans chaque coupon, il te reste à les passer tous en revue pour trouver la faute de frappe.

Une classe (la définition d'un type d'objet que l'on crée avec new) s'écrit class Coupon { ... }, avec à l'intérieur un constructor (la partie appelée par new), sans le mot-clé function.

Dans le constructor, this est la nouvelle instance (un objet créé à partir d'une classe), et new la renvoie même sans return.

// Définir la forme d'un coupon en un seul endroit avec une classe
class Coupon {
  constructor(code, discount) {
    this.code = code;   // À gauche, la propriété de l'instance ; à droite, l'argument reçu
    this.discount = discount;
  }
}

// Créer les coupons un par un avec new
const welcome = new Coupon("WELCOME", 500);
const summer = new Coupon("SUMMER", 1000);
console.log(welcome.code);                // WELCOME
console.log(summer.discount);             // 1000
console.log(welcome instanceof Coupon);   // true

// Sans new, l'appel lève une erreur
// Coupon("SPRING", 300);                 // TypeError
Avec ou sans new : deux issues
Définition declass Couponnew Coupon("WELCOME", 500)new Coupon("SUMMER", 1000)Coupon("SPRING", 300)this.code reçoit"WELCOME"this.code reçoit"SUMMER"Sans new :constructorjamais exécutéwelcome reçoitl'instancesummer reçoitune autre instanceLève uneTypeErrortransmettransmet
Les arguments sont écrits dans this à l'intérieur du constructor. Sans new, l'appel échoue avant même d'entrer dans le constructor.

Les noms de clés n'apparaissent qu'à un seul endroit, dans le constructor : une faute de frappe comme discont ne peut se trouver que là, et c'est le seul endroit à corriger. Dans le cas de la colonne de droite, la console affiche TypeError: Class constructor Coupon cannot be invoked without 'new'.

Crée des données produit avec une classe. productName et productPrice sont déjà déclarés.

① Définis Product pour que chaque instance contienne un nom de produit et un prix.

② Passe les deux variables pour créer mouse.

③ Affiche mouse sous la forme « Nom : prix yens ».

④ Appelle Product sans new dans un bloc try, et affiche la propriété name de l'erreur.

(Si le code s'exécute correctement, une explication s'affiche.)

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Calculer avec les valeurs d'une instance — les méthodes et this

Si tu calcules le montant après réduction avec total - welcome.discount à la fois sur l'écran du panier et sur celui de confirmation de commande, chaque changement de règle de calcul t'oblige à corriger deux endroits. Même si tu déplaces ce calcul dans une fonction, les champs restent dans la classe et le calcul se trouve ailleurs.

Une méthode (une fonction qui appartient à un objet et qu'on appelle avec un point, comme welcome.apply(3000)) s'écrit apply(total) { ... } entre les accolades de la classe, là encore sans function. Dans une méthode, this désigne l'instance écrite à gauche du point, et this.discount lit sa valeur au moment de l'appel.

class Coupon {
  constructor(code, discount) {
    this.code = code;
    this.discount = discount;
  }

  // Soustrait la réduction de ce coupon du total et renvoie le résultat
  apply(total) {
    return total - this.discount;   // this est l'instance à gauche du point
  }
}

// Crée deux types de coupons
const welcome = new Coupon("WELCOME", 500);
const summer = new Coupon("SUMMER", 1000);

// Le même apply donne un résultat différent selon l'instance à gauche du point
console.log(welcome.apply(3000));   // 2500
console.log(summer.apply(3000));    // 2000
Ce qui est à gauche du point devient this
welcome.apply(3000)this vautwelcomethis.discountvaut 500Renvoie 2500summer.apply(3000)this vautsummerthis.discountvaut 1000Renvoie 2000
this est la variable écrite à gauche de .apply. Avec le même argument, un this différent donne un résultat différent.
Portée des arguments du constructor
Dans class Coupon
Dans constructor(code, discount)
  • Arguments code / discount — utilisables seulement dans ces accolades
  • this.discount = discount — stocke la valeur dans l'instance
Dans apply(total)
  • this.discount — lit la valeur stockée dans l'instance
  • total - discount — nom invisible, donc ReferenceError
Le nom discount n'est plus visible hors des accolades du constructor. apply lit à la place la valeur stockée dans this.

Si tu oublies this. dans apply et que tu écris total - discount, JavaScript cherche le nom discount en partant des accolades les plus intérieures vers l'extérieur. Le résultat dépend donc de l'existence, ou non, d'une variable du même nom en dehors de la classe.

Oublier this. ne lève pas toujours d'erreur

S'il existe hors de la classe une variable du même nom, comme const discount = 300;, le total - discount sans this. ne lève pas de ReferenceError : il utilise ces 300 sans rien signaler. Si tous les coupons renvoient le même montant, quel que soit celui sur lequel tu appelles la méthode, soupçonne un this. oublié.

Calcule le prix TTC d'un clavier avec une méthode de Product. Product et keyboard sont déjà déclarés.

① Ajoute à Product une méthode taxIncluded qui renvoie le prix TTC (taxe de 10 %, arrondi à l'unité inférieure).

② Affiche le nom et le prix TTC de keyboard sous la forme « Nom : N yens TTC ».

③ Passe le prix de keyboard à 9800.

④ Affiche-le de nouveau sous la même forme qu'en ②.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Chaque instance garde ses propres valeurs — plusieurs instances

Chaque fois qu'un coupon sert, son nombre d'utilisations restantes diminue de 1. Si tu gardes ces nombres hors de la classe, dans un objet comme { WELCOME: 3, SUMMER: 3 }, chaque écran qui utilise un coupon doit retrouver son compteur à partir de code et y réécrire la nouvelle valeur.

Dans une méthode, tu peux aussi affecter une valeur à une propriété de this, comme dans this.remaining = this.remaining - 1. La partie droite lit la valeur actuelle, et le résultat, diminué de 1, est réécrit dans le remaining de la même instance. Le compteur part des 3 passés à new et diminue de 1 à chaque appel.

class Coupon {
  constructor(code, remaining) {
    this.code = code;
    this.remaining = remaining;   // Nombre d'utilisations restantes
  }

  // Retire une utilisation à l'instance à gauche du point
  use() {
    this.remaining = this.remaining - 1;
  }
}

// Deux coupons avec le même compteur, utilisés un nombre de fois différent
const welcome = new Coupon("WELCOME", 3);
const summer = new Coupon("SUMMER", 3);
welcome.use();
welcome.use();
summer.use();
console.log(welcome.remaining);   // 1
console.log(summer.remaining);    // 2
use() ne modifie que l'instance à gauche du point
welcome.use()deux foisthis vautwelcomeremaining :3 → 2 → 1summer n'estpas modifiésummer.use()une foisthis vautsummerremaining :3 → 2welcome n'estpas modifié
Le use() de welcome et celui de summer ne touchent jamais au compteur de l'autre. Seule l'instance écrite à gauche de use() perd une utilisation.

Tu peux ranger des instances dans un tableau aussi bien que dans des variables. Quand tu appelles une méthode sur un élément, comme coupons[0].use(), this désigne cet élément. Quel que soit le nombre de coupons, le code qui décrémente le compteur tient en une seule ligne, dans use().

Gère le stock de trois produits d'un entrepôt, avec une instance par produit. Product, qui possède une méthode ship pour diminuer le stock, est déjà déclaré.

① Crée les instances pour 12 bras d'écran, 30 hubs USB et 8 supports pour portable, et range-les dans un tableau.

② Expédie 5 unités du premier produit seulement.

③ Affiche « Nom : N en stock » pour chaque produit, à raison d'une ligne par produit.

④ Affiche le stock total sous la forme « Total : N ».

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Choisir selon le nombre d'objets — classe ou littéral objet

Pour des données qui ne servent qu'une fois, comme les paramètres d'affichage d'un écran, écrire { ... } sur place suffit. Mais pour des données comme les coupons, que tu crées en grand nombre avec la même forme et le même calcul, les littéraux t'obligent à recopier jusqu'au corps du calcul dans chacun.

Un littéral objet peut lui aussi avoir des méthodes : écris apply(total) { ... } à côté des paires clé-valeur, et this y désigne cet objet. Ce qui change par rapport à une classe, c'est l'endroit où est écrit le corps de la méthode, et cette différence se voit dans le résultat de Object.keys.

// class : le corps est écrit une seule fois, dans la définition
class Coupon {
  constructor(code, discount) {
    this.code = code;
    this.discount = discount;
  }
  apply(total) { return total - this.discount; }
}
const welcome = new Coupon("WELCOME", 500);

// Littéral : le corps est réécrit dans chaque objet
const summer = {
  code: "SUMMER",
  discount: 1000,
  apply(total) { return total - this.discount; },
};

// Les clés ne sont pas les mêmes
console.log(Object.keys(welcome).join(", "));   // code, discount
console.log(Object.keys(summer).join(", "));    // code, discount, apply

Le corps n'est pas au même endroit, mais tu appelles welcome.apply(3000) exactement comme summer.apply(3000) : le code appelant ne change pas. Comment une instance peut appeler une méthode dont le corps se trouve dans la classe, c'est le sujet de l'article sur les prototypes. Le tableau ci-dessous compare les deux approches.

Point comparéLittéral objetClasse
Objets créésSeul le { } écritUn par new
Corps des méthodesRecopié partoutÉcrit une fois
Clés d'Object.keyscode, discount, applycode, discount
instanceof Couponfalsetrue
Adapté pourConfig ou réponse uniqueNombreux objets

Pas de virgule après les méthodes d'une classe

Quand tu transformes un littéral en classe, si tu laisses la virgule après une méthode, comme dans apply(total) { ... },, tu obtiens SyntaxError: Unexpected token ',' et aucune ligne ne s'exécute. Entre les accolades d'une classe, les méthodes se suivent sans séparateur.

QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Après class Coupon {}, que se passe-t-il si tu appelles Coupon("SPRING") sans new ?

Question 2Une variable discount valant 300 existe hors de la classe. Que renvoie welcome.apply(3000) si this. manque ?

Question 3Que donne Object.keys sur une instance d'une classe qui a constructor(code, discount) et apply ?