Apprenez en lisant dans l'ordre

Fonctions d'ordre supérieur et callbacks — passer et renvoyer des fonctions

Découvre les fonctions d'ordre supérieur, qui reçoivent des fonctions en argument, et les callbacks. forEach, un filter fait maison, des fonctions qui en renvoient.

Il arrive qu'on veuille faire des choses légèrement différentes avec la même liste : simplement afficher chaque élément, la filtrer selon une condition, ou additionner les montants. Ce qu'elles ont en commun, c'est la structure de la boucle ; seul change ce qu'on fait de chaque élément.

Cet article traite des fonctions d'ordre supérieur, qui reçoivent des fonctions en argument, et des callbacks, les fonctions qui leur sont passées puis appelées.

Passer un traitement en argument — les callbacks et forEach

Imagine que tu veuilles afficher une liste de commandes une par une. for...of fait l'affaire, mais il faut alors réécrire la même boucle pour chaque liste, même si seule change la façon d'afficher chaque élément.

Une fonction d'ordre supérieur (une fonction qui reçoit une fonction en argument ou qui renvoie une fonction) te permet de séparer la structure de la boucle de ce qui arrive à chaque élément. Une fonction passée en argument, puis appelée par la fonction qui l'a reçue, s'appelle un callback. La méthode de tableau forEach appelle le callback que tu lui passes une fois pour chaque élément.

const orders = [
  { id: "A-1101", item: "Cahier", amount: 480 },
  { id: "A-1102", item: "Stylo bille", amount: 150 },
  { id: "A-1103", item: "Notes adhésives", amount: 320 },
];

// La fonction ne gère que l'affichage d'un seul élément
const printOrder = (order) => {
  console.log(`${order.id} ${order.item} ${order.amount} yens`);
};

// Passe seulement le nom de la fonction. C'est forEach qui l'appelle
orders.forEach(printOrder);
// A-1101 Cahier 480 yens
// A-1102 Stylo bille 150 yens
// A-1103 Notes adhésives 320 yens
Le même printOrder appelé trois fois
1er élément :A-1101order contientA-1101Affiche A-1101Cahier 480 yens2e élément :A-1102order contientA-1102Affiche A-1102Stylo bille150 yens3e élément :A-1103order contientA-1103Affiche A-1103Notes adhésives320 yens
Tu ne passes qu'une seule fonction, printOrder, et c'est forEach qui l'appelle. D'un appel à l'autre, seul change l'élément contenu dans order.

La ligne orders.forEach(printOrder) ne contient ni condition de boucle ni index. Pour changer la façon d'afficher les éléments, tu ne modifies que printOrder, et tu peux passer la même fonction telle quelle à d'autres listes.

forEach ne peut pas s'arrêter en cours de route

Tu ne peux pas écrire break dans forEach, et un return ne termine que le callback de l'élément en cours : les éléments restants continuent d'être traités. Pour une boucle que tu veux interrompre dès que tu trouves l'élément recherché, utilise for...of avec break.

Affiche une liste de demandes au support une par une. tickets est déjà déclaré.

① Écris une fonction printTicket qui affiche une demande sous la forme « [T-01] Où en est ma livraison ? (traitée) ». Si elle n'est pas encore traitée, affiche (en attente) à la place.

② Passe printTicket à forEach pour afficher toutes les demandes.

③ Compte les demandes traitées et affiche le résultat sous la forme « Traitées : N ».

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

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Recevoir un test sous forme de fonction — un filter fait maison et les booléens

Imagine que tu veuilles filtrer une liste selon différentes conditions : d'abord les produits en stock, puis ceux à 3000 yens ou plus. Si tu écris une boucle séparée pour chaque condition, tu te retrouves avec des copies presque identiques, jusqu'au tableau qui collecte les résultats et à la ligne push.

Seule la ligne qui contient la condition change : reçois-la donc sous forme de fonction de test, et ne garde que les éléments pour lesquels elle renvoie true. Ce qui est passé à un callback, et dans quel ordre, c'est la fonction qui le reçoit et l'appelle qui en décide. Le filterItems ci-dessous ne passe qu'un seul élément à la fonction de test.

const products = [
  { name: "Tapis de bureau", stock: 4 },
  { name: "Support d'écran", stock: 0 },
  { name: "Boîte à câbles", stock: 9 },
];

// Reçoit une fonction de test et ne garde que les éléments pour lesquels elle renvoie true
function filterItems(items, judge) {
  const kept = [];
  items.forEach((item) => {
    if (judge(item)) {
      kept.push(item);
    }
  });
  return kept;
}

const inStock = filterItems(products, (product) => product.stock > 0);
console.log(inStock.length);          // 2
console.log(inStock[0].name);         // Tapis de bureau
La valeur de retour décide où va chaque élément
Tapis de bureau4 en stockTestestock > 04 > 0 vauttrueEntre danskeptSupport d'écran0 en stockTestestock > 00 > 0 vautfalseN'entre pasdans kept
Le produit avec 4 en stock obtient true, celui avec 0 obtient false, et seul le premier entre dans kept. Garder ou non un élément dépend de la valeur renvoyée par la fonction que tu as passée.

Le mot « stock » n'apparaît nulle part dans filterItems. Si tu veux plus tard ne garder que les produits bien approvisionnés, il suffit de changer le test sur la ligne d'appel en product.stock >= 5, sans toucher du tout à filterItems.

Compte les envois d'un planning de livraison qui remplissent une condition. shipments est déjà déclaré.

① Écris countIf, qui reçoit une fonction de test et renvoie le nombre d'éléments qui remplissent la condition.

② Affiche le nombre d'envois expédiés le jour même.

③ Affiche le nombre d'envois dont les frais atteignent 4000 yens ou plus.

④ Place dans une variable isNextDay une fonction qui teste les envois qui ne partent pas le jour même, puis passe-la à countIf et affiche le résultat.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Passer un test que tu as construit — les fonctions d'ordre supérieur qui renvoient des fonctions

Il arrive qu'on passe à filterItems des tests qui ne diffèrent que par la catégorie comparée : « papeterie seulement », « vaisselle seulement », etc. Si tu réécris l'expression du test à chaque appel, le jour où tu renommes la propriété comparée, il faut modifier chacun de ces appels.

L'autre forme de fonction d'ordre supérieur, c'est la fonction qui renvoie une fonction. Elle a la même forme que les fabriques de fonctions de l'article précédent : tu construis byCategory, qui reçoit une catégorie et renvoie une fonction de test. Le test renvoyé garde category en mémoire grâce à une closure, donc tu peux le passer directement à filterItems.

const items = [
  { name: "Cahier", category: "Papeterie" },
  { name: "Mug", category: "Vaisselle" },
  { name: "Stylo bille", category: "Papeterie" },
];

// Reçoit une catégorie et renvoie une fonction qui teste si un article en fait partie
function byCategory(category) {
  return (item) => item.category === category;
}

// Rien n'est encore comparé : ce qui revient, c'est la fonction de test
console.log(typeof byCategory("Papeterie"));   // function

// Passe le test renvoyé directement au filterItems de la section précédente
console.log(filterItems(items, byCategory("Papeterie")).length);   // 2
console.log(filterItems(items, byCategory("Vaisselle")).length);   // 1
Où chaque valeur du test est fixée
Niveau le plus extérieur (hors de toute accolade)
  • filterItems(items, byCategory("Papeterie")) — construit le test et le passe en 2e argument
Dans byCategory
  • category — reçoit "Papeterie" sur la ligne qui l'appelle
  • return (item) => ... — renvoie la fonction de test sans rien comparer
Dans la fonction de test renvoyée
  • item — reçoit un élément passé par filterItems
  • item.category === category — compare avec la valeur de category gardée en mémoire
Dans filterItems
  • judge — contient la fonction de test renvoyée par byCategory
  • judge(item) — passe les éléments un par un et appelle le test
category est fixé sur la ligne qui appelle byCategory, et item à l'intérieur de filterItems. L'endroit qui construit le test et celui qui l'appelle sont différents.

La ligne qui exécute byCategory("Papeterie") ne compare encore rien : la comparaison n'a lieu que lorsque judge(item) est appelé dans filterItems. Avec 3 produits, le même test est appelé 3 fois, et category reste "Papeterie" pendant que seul item change.

Sans parenthèses, tous les éléments sont gardés

Si tu écris filterItems(items, byCategory), chaque produit est passé à byCategory en tant que category, et c'est une fonction qui revient comme résultat du test. Une fonction est truthy (traitée comme true dans une condition), donc chaque produit est gardé. Ce qu'il faut passer, c'est le test renvoyé par byCategory("Papeterie").

Filtre les commandes éligibles à la livraison gratuite réservée aux membres. orders et filterItems sont déjà déclarés.

① Écris both, qui reçoit deux tests et renvoie un test qui ne renvoie true que si les deux sont remplis.

② Stocke deux tests dans des variables : l'un pour les commandes de 5000 yens ou plus, l'autre pour les membres.

③ Combine les tests de ② avec both, passe le résultat à filterItems, et affiche l'ID de chaque commande restante.

④ Applique le test de ③ à la deuxième commande, et affiche le résultat.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Une seule structure pour tout — une fonction générique au traitement interchangeable

Imagine que tu aies écrit deux fonctions qui totalisent les frais de port, l'une pour la livraison standard, l'autre pour l'express. La boucle et la variable qui contient le total sont identiques ; seule diffère l'expression qui calcule les frais de chaque commande. Avec cette organisation, il est facile de corriger l'une et d'oublier de corriger l'autre.

Transforme la partie qui diffère en paramètre de type fonction, et ne garde qu'un seul exemplaire de la structure. sumShipping reçoit un tableau de commandes et une fonction qui calcule les frais, lui passe chaque commande, et additionne les valeurs renvoyées. Le côté appelant n'écrit que l'expression des frais, et le côté qui reçoit ne contient que la boucle et le total.

const shippingOrders = [{ id: "A-1101", total: 6200 }, { id: "A-1102", total: 3400 }];

// Une seule structure. Seul le calcul des frais est reçu sous forme de fonction
function sumShipping(orders, calcFee) {
  let total = 0;
  for (const order of orders) {
    total = total + calcFee(order);
  }
  return total;
}

const normalFee = (order) => (order.total >= 5000 ? 0 : 500);
const expressFee = () => 800;   // N'utilise pas order, donc le paramètre peut être omis
const flatFee = () => 300;

console.log(sumShipping(shippingOrders, normalFee));    // 500
console.log(sumShipping(shippingOrders, expressFee));   // 1600
console.log(sumShipping(shippingOrders, flatFee));      // 600
Trois fonctions de frais, une seule structure
sumShipping(orders,calcFee) : uniquecalcFee reçoitnormalFeecalcFee reçoitexpressFeecalcFee reçoitflatFee0 dès 5000 yens,sinon 500800 quelle quesoit la commande300 quelle quesoit la commandeTotal : 500Total : 1600Total : 600
Selon la fonction passée comme calcFee, les deux mêmes commandes totalisent 500, 1600 ou 600. Le code de sumShipping est identique les trois fois.

Même une modification comme plafonner le total ne demande qu'une seule retouche dans sumShipping, et elle s'applique aux trois types de frais. Le tableau ci-dessous montre comment chacune des trois fonctions réceptrices de cet article appelle son callback et utilise la valeur renvoyée.

Fonction qui reçoitAppel du callbackRetour du callback
forEachLui passe chaque élémentPas utilisée
filterItems fait maisonAppelle judge(item)Garde les éléments à true
sumShipping fait maisonAppelle calcFee(order)Ajoutée au total des frais

À partir des relevés de service d'un employé à temps partiel, affiche la paie de chaque service et calcule le total. shifts est déjà déclaré.

① Écris payReport, qui reçoit les relevés et une fonction qui calcule la paie, affiche chaque service sous la forme « N heures : N yens », puis renvoie le total.

② Passe un calcul à 1100 yens de l'heure, et affiche le total renvoyé sous la forme « Total : N yens ».

③ Passe un calcul où seuls les services de nuit sont majorés de 25 %, et affiche le total sous la même forme.

É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 1Quand tu écris orders.forEach(printOrder), qu'est-ce qui appelle printOrder ?

Question 2Dans le filterItems fait maison, que devient un élément quand la fonction de test que tu as passée renvoie false ?

Question 3Si tu passes byCategory sans parenthèses, comme dans filterItems(items, byCategory), que contient le tableau renvoyé ?