Question 1Pour draft, issu d'une classe sans aucune des trois méthodes, que se passe-t-il quand tu exécutes const [firstLine] = draft; ?
Méthodes spéciales — toString, toJSON et Symbol.iterator
Les méthodes que JavaScript appelle pour toi : toString pour l'affichage, toJSON pour le contenu JSON, Symbol.iterator pour for...of.
Un objet commande s'affiche [object Object] dans un littéral de gabarit, perd son total calculé par un getter dans JSON.stringify, et lève une TypeError dans une boucle for...of.
Cet article présente trois méthodes spéciales (des méthodes que JavaScript cherche par leur nom et appelle dans des situations précises) : celle de l'affichage, celle de la conversion en JSON et celle de l'itération.
Choisir l'affichage d'un objet dans une chaîne — toString
Tu insères une commande dans une notification en écrivant Reçue : ${order}. Au lieu du numéro de commande ou du montant, tu vois [object Object]. Si tu reconstruis le texte à partir de order.id et order.total sur chaque écran, le même code d'assemblage se retrouve partout.
toString (la méthode que JavaScript appelle pour convertir un objet en chaîne, et dont il utilise la valeur de retour) est appelée par ${}, String() et la méthode join des tableaux. Si ta classe n'en définit pas, c'est le toString par défaut d'Object.prototype, plus haut dans la chaîne, qui renvoie [object Object].
// Une classe sans toString
class DraftOrder {
constructor(id, total) { this.id = id; this.total = total; }
}
// Une classe avec toString
class Order {
constructor(id, total) { this.id = id; this.total = total; }
toString() { return `Commande ${this.id} (${this.total} yens)`; }
}
const draft = new DraftOrder("A-101", 3200);
const order = new Order("A-102", 4800);
// Dans un littéral de gabarit, toString est appelé
console.log(`Reçue : ${draft}`); // Reçue : [object Object]
console.log(`Reçue : ${order}`); // Reçue : Commande A-102 (4800 yens)
// String() et le join des tableaux utilisent le même toString
console.log(String(order)); // Commande A-102 (4800 yens)
console.log([order, new Order("A-103", 1500)].join(" / ")); // Commande A-102 (4800 yens) / Commande A-103 (1500 yens)
Le côté appelant, ${}, est le même dans les deux rangées. Pour changer le format d'affichage, tu modifies le toString de la classe à un seul endroit, et chaque ligne qui insère l'objet en tient compte. Quand tu passes l'instance telle quelle, comme dans console.log(order), toString n'est pas appelé.
Inclure le total dans le JSON enregistré — toJSON
Pour envoyer une commande à un serveur, tu la convertis avec JSON.stringify(order). Dans une classe qui garde ses lignes dans #lines et calcule le total avec un getter total, aucun des deux n'est une propriété ordinaire de l'instance : seul {"id":"A-102"} est envoyé.
Si tu écris toJSON (la méthode que JSON.stringify appelle d'abord, pour convertir sa valeur de retour à la place de l'objet), tu peux renvoyer un objet qui contient exactement les valeurs à envoyer. Si un objet Date devient une chaîne de date et d'heure en JSON, c'est justement parce que Date possède cette méthode.
class Order {
#lines; // Lignes de commande (illisibles de l'extérieur)
constructor(id, lines) { this.id = id; this.#lines = lines; }
get total() { return this.#lines.reduce((sum, line) => sum + line.price * line.quantity, 0); }
// Appelée par JSON.stringify : c'est la valeur renvoyée qui est convertie
toJSON() { return { id: this.id, itemCount: this.#lines.length, total: this.total }; }
}
const order = new Order("A-102", [{ price: 1200, quantity: 2 }, { price: 2400, quantity: 1 }]);
const other = new Order("A-103", [{ price: 1500, quantity: 1 }]);
// La seule clé propre de l'instance est id
console.log(Object.keys(order).join(", ")); // id
// C'est la valeur renvoyée par toJSON qui est convertie
console.log(JSON.stringify(order)); // {"id":"A-102","itemCount":2,"total":4800}
// Dans un tableau, toJSON est appelée pour chaque élément
console.log(JSON.stringify([order, other]));
// [{"id":"A-102","itemCount":2,"total":4800},{"id":"A-103","itemCount":1,"total":1500}]
#lines reste illisible de l'extérieur : c'est la classe elle-même qui choisit les valeurs qui partent dans le JSON. Même une valeur que l'instance ne possède pas comme propriété, comme itemCount, peut apparaître dans le JSON si tu l'ajoutes à l'objet renvoyé. Si tu relis la chaîne avec JSON.parse, tu obtiens un objet ordinaire de cette forme, et non une instance d'Order.
Si toJSON renvoie une chaîne, elle est convertie deux fois
Si tu renvoies une chaîne, comme dans return JSON.stringify({ id: this.id, total: this.total });, cette chaîne est à nouveau convertie en JSON. Après un seul JSON.parse, tu as encore une chaîne, et .total vaut undefined. Renvoie plutôt un objet.
Parcourir les lignes de commande avec for...of — Symbol.iterator
Tu veux maintenant afficher les lignes d'une commande à l'écran, une par rangée. Elles sont cachées dans #lines, donc illisibles de l'extérieur, et écrire for (const line of order) lève TypeError: order is not iterable (l'objet n'est pas itérable). Même si tu gardais les lignes dans un tableau public, tu ne pourrais toujours pas parcourir l'instance elle-même.
Si tu définis une méthode nommée Symbol.iterator (une valeur intégrée et unique, qui sert de nom à la méthode qu'appelle for...of), for...of obtient les valeurs une par une en appelant next() sur ce que renvoie cette méthode.
Dans une classe, une méthode génératrice function* nom() s'écrit *nom(). Si tu écris le nom entre crochets, comme [Symbol.iterator], c'est la valeur entre les crochets qui sert de nom à la méthode.
class Order {
#lines; // Lignes de commande (illisibles de l'extérieur)
constructor(id, lines) { this.id = id; this.#lines = lines; }
// La méthode qu'appelle for...of. Chaque ligne passée à yield arrive dans la variable de boucle
*[Symbol.iterator]() {
for (const line of this.#lines) yield line;
}
}
const order = new Order("A-102", [{ name: "Café filtre", quantity: 2 }, { name: "Mug", quantity: 1 }]);
// On récupère les lignes une par une depuis l'extérieur de la classe
for (const line of order) {
console.log(`${line.name} × ${line.quantity}`); // 2 lignes : Café filtre × 2 / Mug × 1
}
// Chaque décomposition rappelle la méthode : la deuxième obtient aussi 2 éléments
console.log([...order].length, [...order].length); // 2 2
Un générateur parcouru jusqu'au bout ne produit plus rien, comme on l'a vu dans l'article sur les itérateurs et les générateurs. L'appelant ne reçoit que les lignes passées à yield, jamais le tableau #lines lui-même : il ne peut donc ni ajouter ni supprimer de lignes depuis l'extérieur.
Renvoyer le tableau lui-même casse for...of
Si tu renvoies le tableau, comme dans return this.#lines;, for...of lève TypeError: undefined is not a function, car un tableau n'a pas de next(). Un tableau est itérable, mais ce n'est pas un itérateur. Ajoute * et passe chaque élément à yield.
Retrouver la méthode manquante à partir du résultat — les trois méthodes
Quand l'affichage ou le JSON ne ressemble pas à ce que tu attendais, des lignes comme ${order} ou JSON.stringify(order) ne mentionnent pas le nom de la méthode qu'elles appellent. Pour savoir quelle méthode tu as oublié d'écrire, remonte à partir de la façon dont l'objet est utilisé et de la forme du résultat.
Même sur l'instance d'une classe qui n'a aucune des trois méthodes, chaque syntaxe cherche le nom qui lui est propre le long de la chaîne de prototypes. La seule qu'elle trouve est le toString par défaut d'Object.prototype ; toJSON et [Symbol.iterator] restent introuvables même en remontant jusqu'à null.
// Une classe sans toString, ni toJSON, ni [Symbol.iterator]
class DraftOrder {
#lines;
constructor(id, lines) { this.id = id; this.#lines = lines; }
get total() { return this.#lines.reduce((sum, line) => sum + line.price * line.quantity, 0); }
}
const draft = new DraftOrder("A-101", [{ name: "Café filtre", price: 1200, quantity: 2 }]);
// toString est trouvé dans Object.prototype ; sans toJSON, seules les clés propres sont listées
console.log(`${draft}`); // [object Object]
console.log(JSON.stringify(draft)); // {"id":"A-101"}
// [Symbol.iterator] n'existe nulle part : une erreur est levée
try {
console.log([...draft]);
} catch (error) {
console.log(error.message); // draft is not iterable
}
JSON.stringify ne lève pas d'erreur et renvoie {"id":"A-101"} : des données sans les lignes de #lines ni le total peuvent donc partir sans que personne ne s'en aperçoive avant le destinataire. Le tableau ci-dessous liste d'autres syntaxes qui cherchent les mêmes noms, et ce qui se passe quand la méthode n'est pas écrite.
| Code | Méthode cherchée | Résultat sans la méthode |
|---|---|---|
| JSON.stringify([draft]) | toJSON (par élément) | [{"id":"A-101"}] |
| for (const line of draft) | [Symbol.iterator] | TypeError: draft is not iterable |
| const [firstLine] = draft | [Symbol.iterator] | TypeError: draft is not iterable |
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Que se passe-t-il si tu appliques JSON.stringify à un tableau de deux commandes dont toJSON renvoie { id: this.id } ?
Question 3Si tu écris deux fois [...cart] pour un cart doté de *[Symbol.iterator](), quelle est la longueur du deuxième tableau ?