Question 1Quand tu écris orders.forEach(printOrder), qu'est-ce qui appelle printOrder ?
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
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.
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
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.
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
filterItems(items, byCategory("Papeterie"))— construit le test et le passe en 2e argument
category— reçoit "Papeterie" sur la ligne qui l'appellereturn (item) => ...— renvoie la fonction de test sans rien comparer
item— reçoit un élément passé parfilterItemsitem.category === category— compare avec la valeur decategorygardée en mémoire
judge— contient la fonction de test renvoyée parbyCategoryjudge(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").
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
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çoit | Appel du callback | Retour du callback |
|---|---|---|
| forEach | Lui passe chaque élément | Pas utilisée |
| filterItems fait maison | Appelle judge(item) | Garde les éléments à true |
| sumShipping fait maison | Appelle calcFee(order) | Ajoutée au total des frais |
Vérification des connaissances
Répondez à chaque question une par une.
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é ?