Cours JavaScript et TypeScript · Module 12: Programmation fonctionnelle
Fonctions d'ordre supérieur : map, filter, reduce
Dans cette leçon5
Le briefing du matin exige trois récapitulatifs à la fois : la liste des noms de tous les spécimens, l'inventaire des prédateurs malades et le nombre total de dinosaures dans le parc. Trois boucles for, ce sont trois compteurs, trois tableaux temporaires et trois occasions de se tromper d'un indice. On peut faire plus court, et sans toucher aux données sources.
Dans les laboratoires de Jurassic Park, les scientifiques traitent chaque jour d'énormes quantités de données sur les dinosaures : ils filtrent les échantillons d'ADN, transforment les résultats d'analyse et agrègent des statistiques. Les fonctions d'ordre supérieur (higher-order functions) sont les outils qui te permettent de faire la même chose avec des données en JavaScript - de manière élégante, lisible et sûre.
Qu'est-ce qu'une fonction d'ordre supérieur ?
Une fonction d'ordre supérieur est une fonction qui accepte une autre fonction en argument ou renvoie une fonction comme résultat. C'est comme une machine de laboratoire qui reçoit une instruction (une fonction) et l'applique à chaque échantillon d'ADN.
Tu en connais déjà une : dans la leçon précédente, la méthode filter recevait la fonction isCarnivore. L'instruction transmise s'appelle un callback : la méthode l'appelle elle-même pour chaque élément du tableau, et toi, tu décides seulement de ce qui doit arriver à un élément.
Array.map() - Transformer les données
map crée un nouveau tableau en appliquant la fonction donnée à chaque élément. Le tableau d'origine reste intact.
Nous avons trois spécimens, et il nous faut uniquement la liste de leurs noms :
1const specimens = [
2 { name: 'Rex', weight: 8000, age: 5 },
3 { name: 'Blue', weight: 150, age: 3 },
4 { name: 'Brachio', weight: 56000, age: 12 },
5];
6
7// Transformation - extraire uniquement les noms
8const names = specimens.map((dino) => dino.name);
9// ['Rex', 'Blue', 'Brachio']Le résultat a toujours autant d'éléments que le tableau d'entrée, car map ne rejette rien, il se contente de transformer. specimens contient toujours les objets complets.
Le callback peut aussi construire un tout nouvel objet. Remarque les parenthèses autour des accolades : sans elles, JavaScript prendrait { pour le début du corps de la fonction, et non pour un littéral d'objet.
1// Transformation - calcul d'un rapport
2const reports = specimens.map((dino) => ({
3 label: dino.name.toUpperCase(),
4 weightInTons: dino.weight / 1000,
5 category: dino.weight > 5000 ? 'large' : 'small',
6}));
7// [{ label: 'REX', weightInTons: 8, category: 'large' }, ...]Chaque rapport est un nouvel objet : toUpperCase met le nom en majuscules, le poids est exprimé en tonnes, et l'opérateur conditionnel ? : attribue une catégorie. Une mise en garde : si le callback de map ne renvoie rien, tu obtiendras un tableau rempli de undefined. Pour les seuls effets de bord, comme une entrée dans la console, on utilise forEach.
Array.filter() - Sélectionner les données
filter crée un nouveau tableau avec les éléments qui remplissent une condition (un prédicat). C'est comme filtrer des échantillons d'ADN : tu ne gardes que ceux qui passent le contrôle qualité.
Un prédicat est une fonction qui renvoie true ou false. Faisons passer un troupeau de quatre dinosaures par deux filtres :
1const allDinosaurs = [
2 { name: 'Rex', diet: 'carnivore', health: 95 },
3 { name: 'Brachio', diet: 'herbivore', health: 88 },
4 { name: 'Raptor', diet: 'carnivore', health: 42 },
5 { name: 'Stego', diet: 'herbivore', health: 100 },
6];
7
8// Filtrage - uniquement les dinosaures en bonne santé
9const healthy = allDinosaurs.filter((dino) => dino.health > 80);
10// [Rex, Brachio, Stego]
11
12// Filtrage - prédateurs qui ont besoin d'aide
13const sickCarnivores = allDinosaurs.filter(
14 (dino) => dino.diet === 'carnivore' && dino.health < 50
15);
16// [Raptor]Le premier filtre garde trois spécimens, le second, qui combine deux conditions avec l'opérateur &&, seulement Raptor. filter renvoie toujours un tableau, même avec un seul élément ou vide, jamais un simple true : [10, 20, 30].filter((x) => x > 15) donne [20, 30]. Les objets ne sont d'ailleurs pas copiés : healthy[0] est le même objet que allDinosaurs[0].
Array.reduce() - Agréger les données
reduce réduit un tableau à une seule valeur, en cumulant le résultat élément par élément. C'est comme un rapport de synthèse : il rassemble les données de tous les enclos et en tire un seul récapitulatif.
La méthode accepte deux arguments : une fonction de combinaison et une valeur initiale. La fonction reçoit l'accumulateur (le résultat obtenu jusque-là) et l'élément courant, puis renvoie le nouvel accumulateur :
1const parkData = [
2 { zone: 'A', dinosaurs: 5, incidents: 0 },
3 { zone: 'B', dinosaurs: 3, incidents: 2 },
4 { zone: 'C', dinosaurs: 8, incidents: 1 },
5];
6
7// Somme de tous les dinosaures
8const totalDinos = parkData.reduce(
9 (sum, zone) => sum + zone.dinosaurs, 0
10);
11// 16L'accumulateur démarre à 0, puis grandit : 0 + 5 = 5, 5 + 3 = 8, 8 + 8 = 16. Indique toujours la valeur initiale. Sans elle, reduce prendra le premier élément comme accumulateur (ici, tout l'objet de la zone A, donc le résultat serait un texte absurde), et sur un tableau vide, il lèvera une TypeError.
L'accumulateur peut aussi être un objet. Regroupons les zones selon leur niveau de sécurité :
1// Regroupement des zones selon la sécurité
2const grouped = parkData.reduce((groups, zone) => {
3 const key = zone.incidents === 0 ? 'safe' : 'risky';
4 return { ...groups, [key]: [...(groups[key] || []), zone.zone] };
5}, {});
6// { safe: ['A'], risky: ['B', 'C'] }L'écriture [key]: est un nom de propriété calculé : la clé est prise dans la valeur de la variable key, c'est-à-dire 'safe' ou 'risky'. L'expression groups[key] || [] renvoie la liste existante, ou un tableau vide quand le groupe n'existe pas encore. Chaque étape construit un nouvel objet de groupes au lieu de modifier le précédent. Dans les environnements récents (ES2024), il existe aussi un Object.groupBy tout prêt, mais il vaut la peine de comprendre ce qu'il fait en coulisses.
Combiner map, filter et reduce
La vraie force de la PF apparaît quand on combine ces fonctions en un pipeline de traitement. filter et map renvoient des tableaux, donc on appelle directement la méthode suivante sur le résultat :
1const specimens = [
2 { id: 'S001', species: 'T-Rex', viability: 0.95, cost: 50000 },
3 { id: 'S002', species: 'Raptor', viability: 0.3, cost: 20000 },
4 { id: 'S003', species: 'Brachio', viability: 0.88, cost: 80000 },
5 { id: 'S004', species: 'Stego', viability: 0.72, cost: 35000 },
6 { id: 'S005', species: 'Raptor', viability: 0.91, cost: 22000 },
7];
8
9// Pipeline : filtrer viability > 0.7 -> calculer le coût -> additionner
10const totalViableCost = specimens
11 .filter((s) => s.viability > 0.7)
12 .map((s) => s.cost * s.viability)
13 .reduce((total, cost) => total + cost, 0);
14// 47500 + 70400 + 25200 + 20020 = 163120Le filtre ne rejette que l'échantillon S002, map transforme les quatre autres en coût pondéré par la viabilité, et reduce les réduit à un seul nombre. L'ordre compte : filtre d'abord, transforme ensuite, pour que map ne calcule rien pour les échantillons rejetés. Et quand un reduce enfle jusqu'à une bonne dizaine de lignes, découpe-le en étapes plus petites - la lisibilité compte plus que l'astuce. Dans la leçon suivante, tu remplaceras une telle chaîne par un pipeline nommé, construit avec pipe.
La prochaine fois que tu seras face à un tableau de données, pense aux trois machines du laboratoire - filter sélectionne les échantillons qui remplissent la condition, map transforme chacun d'eux, et reduce réduit le tout à un seul rapport - et aucune d'elles ne touchera au tableau d'origine.
Code de cette leçon : index.js
1// Fonctions d'ordre superieur : map, filter, reduce
2console.log("=== Jurassic Park - Traitement des Donnees ADN ===\n");
3
4const specimens = [
5 { id: "S001", name: "Rex", species: "T-Rex", weight: 8000, diet: "carnivore", health: 95 },
6 { id: "S002", name: "Blue", species: "Velociraptor", weight: 150, diet: "carnivore", health: 88 },
7 { id: "S003", name: "Brachio", species: "Brachiosaurus", weight: 56000, diet: "herbivore", health: 100 },
8 { id: "S004", name: "Stego", species: "Stegosaurus", weight: 5000, diet: "herbivore", health: 72 },
9 { id: "S005", name: "Raptor", species: "Velociraptor", weight: 140, diet: "carnivore", health: 42 },
10 { id: "S006", name: "Trike", species: "Triceratops", weight: 9000, diet: "herbivore", health: 91 },
11];
12
13// MAP - transformation des donnees
14console.log("--- map: Rapports de poids ---");
15const weightReports = specimens.map((s) => ({
16 name: s.name,
17 weightInTons: (s.weight / 1000).toFixed(1) + "t",
18 category: s.weight > 5000 ? "LARGE" : "SMALL",
19}));
20weightReports.forEach((r) => console.log(` ${r.name}: ${r.weightInTons} (${r.category})`));
21
22// FILTER - selection des donnees
23console.log("\n--- filter: Predateurs en bonne sante ---");
24const healthyCarnivores = specimens.filter(
25 (s) => s.diet === "carnivore" && s.health > 80
26);
27healthyCarnivores.forEach((s) => console.log(` ${s.name}: health=${s.health}`));
28
29console.log("\n--- filter: Dinosaures necessitant des soins ---");
30const needCare = specimens.filter((s) => s.health < 80);
31needCare.forEach((s) => console.log(` ${s.name}: health=${s.health}`));
32
33// REDUCE - agregation
34console.log("\n--- reduce: Statistiques ---");
35const totalWeight = specimens.reduce((sum, s) => sum + s.weight, 0);
36console.log("Poids total:", totalWeight, "kg");
37
38const avgHealth = specimens.reduce((sum, s) => sum + s.health, 0) / specimens.length;
39console.log("Sante moyenne:", avgHealth.toFixed(1));
40
41// Regroupement avec reduce
42const byDiet = specimens.reduce((groups, s) => {
43 const key = s.diet;
44 return { ...groups, [key]: [...(groups[key] || []), s.name] };
45}, {});
46console.log("Par regime:", byDiet);
47
48// PIPELINE - chaine d'operations
49console.log("\n--- Pipeline: Cout d'entretien des grands dinosaures en bonne sante ---");
50const maintenanceCost = specimens
51 .filter((s) => s.health > 80) // uniquement les sains
52 .filter((s) => s.weight > 1000) // uniquement les grands
53 .map((s) => ({ name: s.name, cost: s.weight * 2 })) // cout = 2x poids
54 .reduce((total, s) => {
55 console.log(` ${s.name}: $${s.cost}`);
56 return total + s.cost;
57 }, 0);
58console.log("TOTAL:", "$" + maintenanceCost);Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Que fait la méthode Array.map() en JavaScript ?
2. Que renverra [1,2,3,4,5].filter(x => x > 3) ?
Tâches pratiques dans le jeu
- Éditeur de code
Complète les fonctions calculateThreatIndex, classifyThreat et createReport pour qu'elles soient pures.
- Clique dans l'ordre
Ordonne les éléments d'un appel reduce qui additionne un tableau :
- Éditeur de code
Utilise map() pour extraire les noms, filter() pour la sélection, reduce() pour l'agrégation, puis combine-les en un pipeline.