Apprenez en lisant dans l'ordre

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 est remplacé à chaque appel
Appel 1 :total vaut 0Renvoie0 + 3200Appel 2 :total vaut 3200Renvoie3200 + 2480Appel 3 :total vaut 5680Renvoie5680 + 1180totalPricevaut 6860Reçoit la dernièrevaleur renvoyée
Le 3200 renvoyé par l'appel 1 est passé comme 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.

À partir d'une commande de fournitures de bureau, calcule la quantité totale et le coût total de la commande. orderLines est déjà déclaré, avec un produit par ligne.

① Affiche la quantité totale.

② Affiche le coût total, sachant que le coût de chaque ligne vaut prix unitaire × quantité.

③ Affiche le coût moyen par produit, c'est-à-dire le total de ② divisé par le nombre de produits.

(Si tout s'exécute correctement, une explication apparaîtra.)

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
Ce que tu mets au départ circule jusqu'à la fin
Valeur initiale 0(un nombre)Compare maxet video.viewsRenvoie leplus grand nombremaxViewsvaut 15400Valeur initialevideos[0]Compare top.viewset video.viewsRenvoie leplus grand élémenttopVideo.titleest disponible
La ligne du haut ne transmet qu'un nombre, celle du bas transmet l'élément tel quel. Si tu as aussi besoin du titre, prends un élément comme valeur initiale.

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.

À partir des ventes et des coûts du mois de chaque magasin, trouve les magasins au bénéfice le plus élevé et le plus bas. stores est déjà déclaré, et les montants sont exprimés en unités de 10 000 yens.

① Affiche le nom du magasin au bénéfice (ventes − coûts) le plus élevé.

② Affiche le nom du magasin au bénéfice le plus bas.

③ Affiche le bénéfice du magasin de ②.

④ Affiche l'écart de bénéfice entre le magasin de ① et celui de ②.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
Les 3 appels mettent à jour le même objet counts
Appel 1 :counts vaut {}Met Retour à 1et le renvoieAppel 2 :Retour vaut 1Met Livraison à 1et le renvoieAppel 3 : Retour 1et Livraison 1Met Livraison à 2et le renvoie
Chaque appel ajoute 1 à une clé du 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 obtenirValeur initialeCe que renvoie le callback
Un total0Le total précédent plus la valeur
Le plus grand élémentL'élément d'index 0Le plus grand des deux
Un décompte par catégorieUn objet videL'objet avec la clé mise à jour

Calcule le chiffre d'affaires d'une boutique en ligne par moyen de paiement. orders est déjà déclaré, et amount est le montant d'une commande.

① Crée un objet salesByMethod qui a les moyens de paiement pour clés et leurs montants totaux pour valeurs.

② Convertis salesByMethod en chaîne JSON et affiche-la.

③ Affiche la part de la carte bancaire dans le chiffre d'affaires total, en pourcentage (le nombre seul).

④ Affiche le nombre de moyens de paiement différents.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
Un tableau vide, avec et sans valeur initiale
Tableau vide,valeur initiale 0Aucun élémentà passerLe callbackn'est pas appeléLe 0 initial estrenvoyé tel quelTableau vide,sans valeurinitialePas de premierélément à prendrePas de totalde départLève uneTypeError
Le même tableau vide donne des résultats différents selon que tu passes une valeur initiale ou non. Le premier élément n'est nécessaire que s'il n'y a pas de valeur initiale.

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.

Calcule le total des gros remboursements parmi ceux du mois. refunds est déjà déclaré, et les montants sont en yens.

① Affiche le total de tous les remboursements, sans valeur initiale.

② Crée un tableau qui ne garde que les remboursements de 5000 yens ou plus, et affiche combien il y en a.

③ Affiche le total du tableau de ②, en passant une valeur initiale.

④ Affiche le total du tableau de ②, sans valeur initiale.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Qu'est-ce qui est passé en premier argument au callback que tu donnes à reduce ?

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 ?