Question 1Qu'est-ce qui est passé en premier argument au callback que tu donnes à reduce ?
reduce — réduire un tableau à une seule valeur
reduce combine un tableau en une seule valeur : accumulateur et valeur initiale, total, maximum, regroupement, et la TypeError d'un tableau vide.
Imagine que tu dois calculer le total des lignes d'une commande. map garde le même nombre d'éléments et filter se contente d'en retirer : dans les deux cas, tu récupères un tableau. Aucune des deux ne sait combiner un tableau en une seule valeur, comme un total ou un maximum.
Cet article présente reduce, qui combine un tableau en une seule valeur, et la valeur initiale dont elle part.
Combiner un tableau en une seule valeur — reduce et la valeur initiale
Imagine que tu veux afficher le total d'un panier. Avec forEach, la variable qui contient le total doit vivre en dehors de la boucle, là où n'importe quelle ligne suivante peut l'écraser. Et comme sa valeur change à chaque ajout, tu dois la déclarer avec let plutôt qu'avec const.
La méthode de tableau reduce (une méthode qui passe les éléments un par un à un callback et ne renvoie que la dernière valeur de retour) prend un callback dont le premier paramètre est l'accumulateur (le paramètre qui contient la valeur renvoyée par l'appel précédent). Contrairement à map et filter, l'élément arrive en deuxième. Le deuxième argument de reduce lui-même est la valeur initiale (celle que contient l'accumulateur au premier appel).
const cartItems = [
{ name: "Support pour portable", price: 3200 },
{ name: "Hub USB", price: 2480 },
{ name: "Câble HDMI", price: 1180 },
];
// Le premier paramètre est la valeur renvoyée par l'appel précédent, le second l'élément courant
const addPrice = (total, item) => total + item.price;
// Le deuxième argument, 0, devient total au premier appel
const totalPrice = cartItems.reduce(addPrice, 0);
console.log(totalPrice); // 6860
// Ce qui revient est une seule valeur, pas un tableau
console.log(typeof totalPrice); // number
total à l'appel 2. Le 6860 renvoyé par le dernier appel est le résultat de reduce.Tu n'as pas besoin de ta propre variable pour le total en cours, parce que c'est reduce qui se charge de transmettre la valeur d'un appel au suivant. Et ce qui est transmis n'est pas forcément un nombre : ça peut aussi être un élément ou un objet.
Garder le plus grand — transmettre un nombre ou un élément
Imagine que tu veux savoir laquelle de tes vidéos publiées a le plus de vues. Cette fois, tu n'additionnes rien : tu compares les valeurs et tu ne transmets que la plus grande à l'appel suivant. Reste à choisir la valeur de départ de l'accumulateur.
L'accumulateur peut contenir un nombre ou l'élément lui-même. Si tu passes 0 en deuxième argument, c'est un nombre qui circule d'un appel à l'autre ; si tu passes videos[0], c'est l'élément lui-même. La valeur initiale doit être du même genre que ce que renvoie le callback.
const videos = [
{ title: "Les bases de SQL #1", views: 8200 },
{ title: "Les bases de Linux #3", views: 15400 },
{ title: "Les fondamentaux de Git", views: 9700 },
];
// La valeur initiale est un nombre : on compare et on renvoie des nombres
const maxViews = videos.reduce((max, video) => Math.max(max, video.views), 0);
console.log(maxViews); // 15400
// La valeur initiale est un élément : on compare et on renvoie des éléments
const topVideo = videos.reduce(
(top, video) => (video.views > top.views ? video : top),
videos[0],
);
console.log(topVideo.title); // Les bases de Linux #3
Les deux appels à reduce comparent views ; la seule différence, c'est ce qui est transmis. Si tu donnais au second une valeur initiale de 0, top.views vaudrait undefined au premier appel : la comparaison serait false à chaque fois, et c'est 0 qui serait renvoyé jusqu'à la fin.
Une valeur initiale de 0 ne marche pas avec des nombres négatifs
Dans un tableau qui peut contenir des valeurs négatives, comme des relevés de température, partir de 0 signifie que 0 reste la plus grande valeur même quand le vrai maximum est -3 : tu obtiens alors un résultat qui ne figure même pas dans le tableau. Si tu compares les éléments entre eux en partant du premier élément du tableau, la plage des valeurs n'a plus d'importance.
Rassembler des décomptes dans un objet — le regroupement
Imagine que tu veux savoir combien de demandes au support il y a dans chaque catégorie. Contrairement à un total, le résultat n'est pas un seul nombre : il te faut une valeur par catégorie. Tu pourrais créer un objet vide en dehors de la boucle et le remplir avec forEach, mais le décompte resterait là encore en dehors du traitement.
Tu peux aussi prendre {} comme valeur initiale. L'accumulateur est alors l'objet en cours de décompte : le callback met à jour une clé, puis renvoie cet objet. Lire une clé qui n'a pas encore été ajoutée donne undefined : ajoute donc ?? 0 pour commencer le décompte à 0.
const tickets = [
{ subject: "Je voudrais retourner un article", category: "Retour" },
{ subject: "Ma commande n'est pas arrivée", category: "Livraison" },
{ subject: "Merci de renvoyer mon bordereau", category: "Livraison" },
];
// Ajoute 1 au décompte de la catégorie de chaque demande
const countByCategory = tickets.reduce((counts, ticket) => {
counts[ticket.category] = (counts[ticket.category] ?? 0) + 1;
return counts; // Transmet counts mis à jour à l'appel suivant
}, {}); // Ce {} devient counts au premier appel
console.log(JSON.stringify(countByCategory)); // {"Retour":1,"Livraison":2}
console.log(countByCategory["Livraison"]); // 2
counts qu'il reçoit, puis renvoie ce même objet. Le seul objet dans lequel on écrit est celui passé comme valeur initiale.Le {} initial est un nouvel objet qui ne contient que le décompte : rien n'est écrit dans les éléments de tickets. Le tableau ci-dessous résume la valeur initiale et la valeur de retour du callback pour les 3 usages vus jusqu'ici.
| Ce que tu veux obtenir | Valeur initiale | Ce que renvoie le callback |
|---|---|---|
| Un total | 0 | Le total précédent plus la valeur |
| Le plus grand élément | L'élément d'index 0 | Le plus grand des deux |
| Un décompte par catégorie | Un objet vide | L'objet avec la clé mise à jour |
Quand il n'y a aucun élément — reduce sans valeur initiale
Un résultat filtré peut très bien être vide. Si tu calcules le total des expéditions du jour un jour où il n'y en a eu aucune, reduce renvoie 0 ou lève une erreur, selon la façon dont tu l'as écrit.
Tu peux omettre le deuxième argument, la valeur initiale. Dans ce cas, le premier élément du tableau devient l'accumulateur de départ, et le premier appel du callback reçoit l'élément d'index 1. Un tableau vide n'a pas de premier élément à prendre : une TypeError est donc levée.
const yesterdayAmounts = [3200, 2480, 1180];
const todayAmounts = [];
// Sans valeur initiale, le premier élément, 3200, devient le total de départ
console.log(yesterdayAmounts.reduce((total, amount) => total + amount)); // 6860
// Même sur un tableau vide, la valeur initiale est renvoyée telle quelle
console.log(todayAmounts.reduce((total, amount) => total + amount, 0)); // 0
// Sans valeur initiale, impossible de fixer le total de départ
// todayAmounts.reduce((total, amount) => total + amount);
// TypeError: Reduce of empty array with no initial value
Ce genre de bug passe facilement inaperçu quand tu écris le code : tout marche avec les données que tu as sous la main, et ça ne casse que le premier jour où rien ne correspond. Quand tu passes un résultat filtré à reduce, passe une valeur initiale au cas où il serait vide.
Sans {}, c'est le premier élément qui est modifié
Si tu omets le {} final dans l'exemple countByCategory, c'est l'objet de demande tickets[0] lui-même qui devient le counts de départ. Aucune erreur n'est levée : une clé Livraison est ajoutée à cet objet, et l'unique demande Retour n'est jamais comptée. Même quand le tableau n'est pas vide, passe toujours une valeur initiale quand tu rassembles des valeurs dans un objet.
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Si tu passes une valeur initiale de 0 à un reduce qui compare les views des éléments et renvoie le plus grand, qu'est-ce qui revient ?
Question 3Que se passe-t-il quand tu appelles reduce sans valeur initiale sur un tableau vide ?