Les méthodes de tableau JavaScript : map, filter, reduce sans se perdre
Trois méthodes couvrent l'essentiel du travail sur les tableaux en JavaScript : map transforme chaque élément, filter en garde une partie, reduce les ramène à une seule valeur. Sur le papier, c'est simple. En pratique, on obtient un tableau rempli de undefined, un NaN sorti de nulle part ou un total qui s'affiche [object Object]0182560, sans le moindre message d'erreur.
Cette page part d'un même petit tableau de commandes et montre, pour chaque méthode, ce qu'elle renvoie et le piège qui va avec. Il ne s'agit pas d'un cours complet sur les tableaux : sort, flatMap et les autres méthodes restent hors champ, sauf find, some et every, qui répondent à des questions que l'on pose souvent, à tort, à filter.
Tout le code a été exécuté le 7 novembre 2026 sous Windows 11 avec Node 22.23.3. Chaque sortie citée est celle que le script de mesure a obtenue, copiée telle quelle ; la mise en forme (crochets, guillemets simples) est celle de console.log dans Node.
Trois questions pour choisir la méthode
Voici les données de tous les exemples. La commande 2, d'un montant de 0, a été réglée avec un bon d'achat : elle est payée, mais ne rapporte rien.
const commandes = [
{ id: 1, client: "Ada", total: 42, statut: "payée" },
{ id: 2, client: "Linus", total: 0, statut: "payée" },
{ id: 3, client: "Grace", total: 18, statut: "annulée" },
{ id: 4, client: "Ada", total: 25, statut: "payée" },
{ id: 5, client: "Alan", total: 60, statut: "en attente" },
];Avant d'écrire la moindre ligne, je me pose une question sur la forme du résultat. Est-ce que je veux autant d'éléments qu'au départ, chacun transformé ? C'est map. Une partie des éléments, inchangés ? C'est filter. Une seule valeur, nombre, chaîne ou objet ? C'est reduce.
const montants = commandes.map((c) => c.total);
const payees = commandes.filter((c) => c.statut === "payée");
const encaisse = payees.reduce((somme, c) => somme + c.total, 0);
console.log(montants);
console.log(payees.map((c) => c.id));
console.log(encaisse);[ 42, 0, 18, 25, 60 ]
[ 1, 2, 4 ]
67Deux questions reviennent aussi souvent, et aucune des trois méthodes n'est la bonne réponse : « quel est le premier élément qui… » appelle find, et « est-ce qu'au moins un élément… » appelle some. On y revient avec filter, qui est souvent utilisé à leur place.
map : autant d'éléments en sortie qu'en entrée
map appelle votre fonction sur chaque élément et range ce qu'elle renvoie dans un nouveau tableau, à la même position. Le mot important est « renvoie » : une fonction fléchée avec des accolades ne renvoie rien sans return.
const noms = commandes.map((c) => {
c.client.toUpperCase();
});
console.log(noms);
const fiches = commandes.map((c) => { id: c.id });
console.log(fiches);
console.log(commandes.map((c) => c.client.toUpperCase()));
console.log(commandes.map((c) => ({ id: c.id })).at(-1));[ undefined, undefined, undefined, undefined, undefined ]
[ undefined, undefined, undefined, undefined, undefined ]
[ 'ADA', 'LINUS', 'GRACE', 'ADA', 'ALAN' ]
{ id: 5 }Le premier cas est l'oubli classique : le calcul a lieu, puis le résultat est jeté. Le deuxième est plus sournois. On croit renvoyer un objet { id: c.id }, mais après une flèche, une accolade ouvre un bloc d'instructions : id: y devient une étiquette (un label), et la fonction ne renvoie rien. Avec deux propriétés, (c) => { id: c.id, client: c.client }, le code n'est même plus valide et Node refuse le fichier avec « SyntaxError: Unexpected token ':' ». Le remède tient dans les deux dernières lignes : pas d'accolades pour une expression simple, et des parenthèses autour d'un objet, ({ id: c.id }). C'est la forme qu'utilise le quiz interactif en JavaScript pour transformer chaque réponse en objet avant de mélanger.
L'autre piège de map vient de ce qu'elle passe à votre fonction trois arguments, et non un seul : l'élément, son index et le tableau entier.
const saisies = ["10", "10", "10"];
console.log(saisies.map(parseInt));
console.log(saisies.map((s) => parseInt(s, 10)));
console.log(saisies.map(Number));[ 10, NaN, 2 ]
[ 10, 10, 10 ]
[ 10, 10, 10 ]Trois fois la même chaîne, trois résultats différents. parseInt accepte un second argument, la base de numération, et map lui donne l'index à cet endroit : parseInt("10", 0) traite la base 0 comme absente et lit en base 10, parseInt("10", 1) donne NaN (une base doit aller de 2 à 36), parseInt("10", 2) lit du binaire et donne 2. Passer directement une fonction existante à map n'est sûr que si elle ignore les arguments en trop, ce qui est le cas de Number. Dans le doute, j'écris la petite fonction fléchée : elle dit exactement quels arguments partent.
filter garde ce qui est vrai, au sens large
filter garde un élément quand votre fonction renvoie une valeur « truthy », c'est-à-dire tout sauf une valeur « falsy » : false, 0, "", null, undefined, NaN, et quelques cas plus rares comme -0 ou 0n. Le raccourci filter((n) => n), ou son cousin filter(Boolean), retire donc toutes les valeurs « falsy », undefined comme 0 : pratique pour écarter des undefined, dangereux dès qu'un zéro est une vraie valeur.
// undefined = copie pas encore rendue
const notes = [12, 0, undefined, 15];
console.log(notes.filter((n) => n));
console.log(notes.filter(Boolean));
console.log(notes.filter((n) => n !== undefined));[ 12, 15 ]
[ 12, 15 ]
[ 12, 0, 15 ]L'élève qui a eu 0 disparaît des deux premiers résultats, et une moyenne calculée ensuite passerait de 9 à 13,5. La correction consiste à écrire la condition que l'on veut vraiment : ici, « la copie a été rendue », donc n !== undefined. Dans nos commandes, le même raccourci filter((c) => c.total) ferait disparaître la commande 2, payée avec un bon d'achat.
Deuxième malentendu : filter renvoie toujours un tableau, vide si rien ne passe. Or un tableau vide est lui aussi « truthy ».
const deMargaret = commandes.filter((c) => c.client === "Margaret");
if (deMargaret) {
console.log("Margaret a déjà commandé :", deMargaret);
}
console.log(commandes.some((c) => c.client === "Margaret"));
console.log(commandes.find((c) => c.client === "Ada"));
console.log(commandes.find((c) => c.client === "Margaret"));Margaret a déjà commandé : []
false
{ id: 1, client: 'Ada', total: 42, statut: 'payée' }
undefinedLe if passe alors qu'aucune commande de Margaret n'existe. Pour savoir si au moins un élément correspond, some renvoie directement true ou false ; pour obtenir le premier élément qui correspond, find renvoie l'élément lui-même, ou undefined. Les deux s'arrêtent au premier élément trouvé, là où filter parcourt toujours tout le tableau.
reduce : la valeur de départ n'a rien d'un détail
reduce fait circuler une valeur, l'accumulateur, d'un élément au suivant : votre fonction reçoit l'accumulateur et l'élément courant, et ce qu'elle renvoie devient l'accumulateur de l'appel suivant. Le second argument de reduce, la valeur de départ, est facultatif. Voici ce qui se passe quand on l'oublie.
console.log(commandes.reduce((somme, c) => somme + c.total));
console.log(commandes.reduce((somme, c) => somme + c.total, 0));[object Object]0182560
145Sans valeur de départ, l'accumulateur commence avec le premier élément du tableau, ici l'objet de la commande 1, et la fonction démarre au deuxième élément. objet + 0 transforme l'objet en texte, "[object Object]", puis chaque total vient s'y coller comme une chaîne : 0, 18, 25 et 60. Avec 0 pour départ, on obtient la somme des cinq totaux, 145. Le premier piège se voit dès le premier essai ; les deux suivants sont pires, parce qu'ils dépendent du nombre d'éléments.
const aucune = [];
const uneSeule = [{ id: 9, total: 30 }];
try {
aucune.reduce((somme, c) => somme + c.total);
} catch (erreur) {
console.log(`${erreur.name}: ${erreur.message}`);
}
console.log(uneSeule.reduce((somme, c) => somme + c.total));
console.log(aucune.reduce((somme, c) => somme + c.total, 0));
console.log(uneSeule.reduce((somme, c) => somme + c.total, 0));TypeError: Reduce of empty array with no initial value
{ id: 9, total: 30 }
0
30Sur un tableau vide, reduce sans valeur de départ lève une TypeError. Sur un tableau d'un seul élément, il renvoie cet élément sans même appeler votre fonction : on attendait 30, on reçoit l'objet entier. Un code testé avec trois commandes peut donc casser le jour où il n'y en a qu'une, ou aucune. Ma règle est simple : je donne toujours une valeur de départ, du même type que le résultat attendu (0 pour une somme, {} pour un objet, [] pour un tableau). C'est ce que fait la todo-list en React pour calculer le prochain identifiant, reduce((max, t) => Math.max(max, t.id), 0) : sans ce 0, le premier ajout dans une liste vide lèverait cette même TypeError.
Avec un objet pour départ, reduce sert aussi à regrouper. Ici, le total commandé par chaque client, puis la même fonction sans son return :
const parClient = commandes.reduce((cumul, c) => {
cumul[c.client] = (cumul[c.client] ?? 0) + c.total;
return cumul;
}, {});
console.log(parClient);
try {
commandes.reduce((cumul, c) => {
cumul[c.client] = (cumul[c.client] ?? 0) + c.total;
}, {});
} catch (erreur) {
console.log(`${erreur.name}: ${erreur.message}`);
}{ Ada: 67, Linus: 0, Grace: 18, Alan: 60 }
TypeError: Cannot read properties of undefined (reading 'Linus')L'erreur mentionne Linus, le client de la deuxième commande, alors que le défaut est dans la fonction : le premier appel n'a rien renvoyé, donc l'accumulateur du deuxième vaut undefined. Quand une erreur de reduce désigne le deuxième élément, cherchez d'abord le return manquant.
Pour ranger des éléments par catégorie, il existe maintenant plus direct que reduce : Object.groupBy, disponible depuis Chrome 117, Firefox 119, Safari 17.4 et Node 21.0.0.
const parStatut = Object.groupBy(commandes, (c) => c.statut);
console.log(Object.keys(parStatut));
console.log(parStatut["payée"].map((c) => c.id));[ 'payée', 'annulée', 'en attente' ]
[ 1, 2, 4 ]Object.groupBy range les éléments entiers dans des tableaux, un par clé. Pour un cumul (une somme par client, comme plus haut), reduce reste l'outil.
Enchaîner sans toucher au tableau d'origine
Comme map et filter renvoient un tableau, on peut appeler la méthode suivante directement sur leur résultat. Les compteurs ci-dessous montrent ce que fait réellement la chaîne.
const appels = { filter: 0, map: 0, reduce: 0 };
const encaisse = commandes
.filter((c) => {
appels.filter++;
return c.statut === "payée";
})
.map((c) => {
appels.map++;
return c.total;
})
.reduce((somme, total) => {
appels.reduce++;
return somme + total;
}, 0);
console.log(encaisse, appels);67 { filter: 5, map: 3, reduce: 3 }Chaque étape fait son propre passage : filter parcourt les cinq commandes et produit un tableau de trois, map parcourt ces trois et produit un second tableau, puis reduce le parcourt à son tour et renvoie un seul nombre, 67. Une boucle unique ferait cinq tours au lieu de onze. Je n'ai pas mesuré de durée, et sur quelques centaines d'éléments je garde la chaîne : elle se lit de haut en bas, une intention par ligne. Ordre utile à retenir : filtrer d'abord, transformer ensuite, pour que map ne travaille que sur ce qui sera gardé.
La chaîne ne modifie pas le tableau commandes. En revanche, rien n'empêche votre fonction de le faire, et c'est là que map trompe le plus souvent.
const vues = commandes.map((c) => {
c.vue = true;
return c;
});
console.log(commandes[0]);
console.log(vues[0] === commandes[0]);{ id: 1, client: 'Ada', total: 42, statut: 'payée', vue: true }
trueLe nouveau tableau contient les mêmes objets que l'ancien, pas des copies. Modifier c modifie donc la commande d'origine, et les deux tableaux pointent vers les mêmes objets. Pour obtenir un objet neuf, on le recopie avec la syntaxe de décomposition, en ajoutant ou remplaçant la propriété voulue :
const vues = commandes.map((c) => ({ ...c, vue: true }));
console.log(commandes[0]);
console.log(vues[0]);
console.log(vues[0] === commandes[0]);{ id: 1, client: 'Ada', total: 42, statut: 'payée' }
{ id: 1, client: 'Ada', total: 42, statut: 'payée', vue: true }
falseCette copie est superficielle : si une commande contenait un objet imbriqué (une adresse, par exemple), il resterait partagé entre l'original et la copie. C'est exactement le motif de la todo-list en React pour cocher une tâche, { ...t, faite: !t.faite } dans un map : le map fournit un nouveau tableau, que React distingue de l'ancien avec Object.is, et la décomposition évite de modifier la tâche de l'état précédent, partagée sinon entre les deux tableaux.
Sur un tableau vide, chaque méthode répond autrement
Le tableau vide arrive plus souvent qu'on ne le croit : une recherche sans résultat, un filtre trop strict, une API qui ne renvoie rien. Voici ce que chaque méthode renvoie dans ce cas.
const vide = [];
console.log(vide.map((x) => x * 2));
console.log(vide.filter((x) => x > 0));
console.log(vide.find((x) => x > 0));
console.log(vide.some((x) => x > 0));
console.log(vide.every((x) => x > 0));
console.log(vide.reduce((somme, x) => somme + x, 0));[]
[]
undefined
false
true
0| Méthode | Tableau vide | À surveiller |
|---|---|---|
map, filter | [] | Toujours « truthy » dans un if |
find | undefined | Lire une propriété ensuite plante |
some | false | Rien à signaler |
every | true | « Tous valides » sur une liste vide |
reduce | Valeur de départ, ou TypeError sans elle | Toujours donner le départ |
Le true de every surprend toujours la première fois : il n'existe aucun élément qui ne respecte pas la condition, donc elle est respectée par tous. C'est logique, mais un formulaire qui vérifie « tous les champs sont valides » avec every sur une liste de champs vide dira oui. Si une liste vide doit être refusée, testez sa longueur à part.
Avec async, revenir à Promise.all ou à une boucle
Aucune de ces méthodes n'attend une fonction async. Elles reçoivent une promesse là où elles attendent une valeur, et continuent sans attendre.
const attendre = (ms) => new Promise((fin) => setTimeout(fin, ms));
async function chargerTotal(id) {
await attendre(50);
return commandes.find((c) => c.id === id).total;
}
const promesses = [1, 4, 5].map((id) => chargerTotal(id));
console.log(promesses);
console.log(await Promise.all(promesses));
const plusDe30 = [1, 4, 5].filter(async (id) => (await chargerTotal(id)) > 30);
console.log(plusDe30);[ Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]
[ 42, 25, 60 ]
[ 1, 4, 5 ]Avec map, le résultat est un tableau de promesses, ce qui n'a rien de faux : Promise.all les attend toutes et rend les valeurs dans l'ordre. Avec filter, c'est un vrai bug. Une promesse est un objet, donc « truthy », et les trois commandes passent alors que celle à 25 ne devrait pas. La correction : attendre d'abord les valeurs, filtrer ensuite.
const ids = [1, 4, 5];
const totaux = await Promise.all(ids.map((id) => chargerTotal(id)));
const plusDe30 = ids.filter((id, i) => totaux[i] > 30);
console.log(plusDe30);[ 1, 5 ]Le même raisonnement vaut pour reduce, dont l'accumulateur deviendrait une promesse, et pour forEach, qui n'attend rien : l'article fetch et async/await, les erreurs qu'on ne voit pas mesure ce cas sur trois requêtes et compare for...of à Promise.all. Quand les appels doivent se faire l'un après l'autre, ou s'arrêter au premier échec, une boucle for...of avec await reste la forme la plus claire.
Rejouer les exemples
Un script de mesure sans dépendance assemble chaque exemple avec le tableau commandes (et, pour la correction en async, la fonction chargerTotal), l'exécute dans un processus Node séparé comme module ES (fichier .mjs, ce qui autorise le await au niveau supérieur des deux exemples async), puis compare la sortie, normalisée en fins de ligne LF, à celle publiée ici. Il vérifie aussi que chaque bloc de code et chaque sortie de cette page reprennent les fichiers exécutés et leurs sorties, après normalisation des fins de ligne en LF et sans tenir compte des sauts de ligne en fin de fichier. Environnement : Node 22.23.3 (V8 12.4.254.21-node.57), Windows 11, le 7 novembre 2026. Les valeurs renvoyées sont fixées par la norme ECMAScript ; le texte des messages d'erreur et la mise en forme de l'affichage viennent de Node et de son moteur V8, et peuvent différer dans un autre moteur, comme celui de Firefox.
Pages consultées le 7 novembre 2026 :
- MDN, Array.prototype.map() : la fonction reçoit l'élément, l'index et le tableau ; section « Using parseInt() with map() ».
- MDN, parseInt() : une base 0 ou absente est traitée comme 10 (sauf préfixe
0x), une base hors de 2 à 36 donneNaN. - MDN, Falsy : liste des valeurs « falsy ».
- MDN, Arrow function expressions :
returnobligatoire dans un corps entre accolades ; un objet littéral renvoyé s'entoure de parenthèses, sinon ses clés sont lues comme des étiquettes. - MDN, Array.prototype.filter() : élément gardé si la fonction renvoie une valeur « truthy » ; tableau vide si rien ne passe.
- MDN, Array.prototype.find() : renvoie le premier élément trouvé et arrête le parcours, sinon
undefined. - MDN, Array.prototype.some() : renvoie
trueet arrête le parcours au premier élément trouvé. - MDN, Array.prototype.reduce() : sans valeur initiale, l'accumulateur part du premier élément ;
TypeErrorsur un tableau vide ; un seul élément est renvoyé sans appeler la fonction. - MDN, Array.prototype.every() : renvoie
truepour un tableau vide. - MDN, Spread syntax : la décomposition d'un objet en fait une copie superficielle.
- MDN, Object.groupBy() : renvoie un objet dont chaque propriété est le tableau d'un groupe.
- Données de compatibilité de MDN, browser-compat-data (branche principale), fichier
javascript/builtins/Object.json, entréegroupBy: Chrome 117, Firefox 119, Safari 17.4, Node.js 21.0.0.