Depuis un peu plus d’un an que j’écris sur ce blog, je vous ai surtout parlé de Vue.js, ou encore d’outils comme ESLint ; mais tous les bons artisans vous le diront, avoir le meilleur marteau du monde n’interdit pas de s’écraser les doigts !
Aujourd’hui, laissons donc de côté le strass et les paillettes et mettons les
deux mains dans le cambouis : en tant que développeur·se JavaScript, à quel
point maîtrisez-vous votre langage préféré ? Savez-vous vraiment utiliser
Array.prototype.reduce
?
…et c’est bien dommage, car utiliser cette fonction est diablement simple une fois qu’on a compris comment elle marche !
En deux mots, reduce
a pour objectif de transformer un tableau en n’importe
quoi d’autre, en itérant progressivement sur ce tableau et en apportant des
modifications successives à un accumulateur, ce dernier constituant au final
la valeur de retour ! C’est plus clair ?
…Non ? Bon, prenons un exemple tout bête :
const values = [1, 2, 3, 4];
Quelle est selon vous la manière la plus simple de calculer la somme d’un
tableau de Number
?
let sum = 0;
values.forEach(value => {
sum += value;
});
Ça fonctionne, mais il y a plus simple, vous vous en doutez, avec reduce
:
const sum = values.reduce((acc, cur) => acc + cur, 0);
Décomposons cette petite ligne de code :
reduce
prend en premier paramètre une fonction, appelée sur chacun des
éléments de values
acc
)
et une valeur courante (typiquement notée cur
), à savoir l’élément
courant dans values
acc
au tour de
boucle suivant
reduce
donne la valeur de départ d’acc
(qui en a besoin au premier tour de boucle puisque notre fonction n’a pas
encore été exécutée)Vous l’aurez donc compris, au premier tour de boucle, la fonction renverra 1
(0 + 1
), au second 3
(1 + 2
), au troisième 6
(3 + 3
) et au quatrième
10
(6 + 4
), ce qui sera également la valeur de sum
en fin d’exécution !
La notion d’accumulateur doit désormais vous paraître plus claire, et le
fonctionnement de reduce
lui-même également, de fait ! Afin de vous
familiariser avec, voyons ensemble des cas d’utilisation plus complexes, qui
vous démontreront toute sa puissance.
On peut parfois avoir besoin de transformer un objet en tableau :
const obj = {
foo: { value: "bar" },
bar: { value: "baz" }
};
Pour ce faire, nous allons utiliser Object.keys
afin de pouvoir itérer sur le
tableau contenant les clés de notre objet :
Object.keys(obj).reduce((acc, key) => [...acc, { key, value: obj[key].value }], []);
La décomposition (...
) utilisée sur acc
revient au même qu’appeler concat
dessus.
Notre objet initial devient donc le tableau suivant :
[
{ key: "foo", value: "bar" },
{ key: "bar", value: "baz" }
]
La fonction peut également servir à obtenir un booléen après avoir parcouru l’intégralité d’un tableau (ou d’un objet), en validant tel ou tel aspect de chaque élément :
const allInputsAreFilled = Array.from(document.querySelectorAll("input")).reduce((acc, cur) => acc && !!cur.value, true);
reduce
peut aussi être utile dans un tel cas :
const obj = {
foo: "Jean-Pierre",
bar: "Thierry",
baz: "Gaston"
};
const subject = "{foo} dit alors à {bar} que c'était la faute de {baz}.";
Comme dans l’exemple précédent, tirons parti d’Object.keys
:
Object.keys(obj).reduce((acc, cur) => acc.replace(new RegExp(`{${cur}}`), obj[cur]), subject);
Et voici le résultat :
"Jean-Pierre dit alors à Thierry que c'était la faute de Gaston."
Bien évidemment, ce pattern s’avère en pratique plus pertinent sur de la gestion d’URLs, par exemple.
Imaginons ensuite un tableau d’objets, que l’on souhaite agréger en fonction d’une de leurs clés :
const tasks = [
{ id: 1, label: "foo", state: "open" },
{ id: 2, label: "bar", state: "close" },
{ id: 3, label: "baz", state: "open" },
{ id: 4, label: "qux", state: "open" },
{ id: 5, label: "kek", state: "close" }
];
tasks.reduce((acc, cur) => {
if (!(cur.state in acc)) {
acc[cur.state] = [];
}
acc[cur.state].push(cur);
return acc;
}, {});
Ce qui nous donne :
{
open: [
{ id: 1, label: "foo", state: "open" },
{ id: 3, label: "baz", state: "open" },
{ id: 4, label: "qux", state: "open" }
],
close: [
{ id: 2, label: "bar", state: "close" },
{ id: 5, label: "kek", state: "close" }
]
}
En tirant parti de la décomposition, notre reduce
peut par ailleurs être
écrit de manière plus concise :
tasks.reduce((acc, cur) => ({
...acc,
...{ [cur.state]: [...acc[cur.state] || [], cur] }
}), {});
Prenons enfin l’objet suivant, en partant du principe que nous voulons
remplacer toutes les occurrences de "non"
par "oui"
:
const obj = {
foo: "non",
bar: {
baz: "non",
uno: 1,
qux: {
kek: "non",
dos: 2,
arr: [3, "non", { foo: "non", bar: "baz" }]
}
}
};
function processValue(value) {
if (typeof value === "object") { // objet ou tableau
return processValues(value);
}
return value === "non" ? "oui" : value; // le traitement effectif
}
function processValues(values) {
return Object.keys(values).reduce((acc, cur) => {
if (Array.isArray(values[cur])) {
return { ...acc, [cur]: values[cur].map(item => processValue(item)) };
}
return { ...acc, [cur]: processValue(values[cur]) };
}, {});
}
processValues(obj);
Le résultat est à la hauteur de nos espérances :
{
foo: "oui",
bar: {
baz: "oui",
uno: 1,
qux: {
kek: "oui",
dos: 2,
arr: [3, "oui", { foo: "oui", bar: "baz" }]
}
}
}
J’espère de tout cœur que ces quelques exemples vous auront donné une meilleure
idée des cas d’usage d’Array.prototype.reduce
! N’oubliez pas que les
tableaux en JavaScript ont une armada d’autres méthodes
à disposition, lesquelles se combineront fort efficacement avec celle que nous
venons de voir (map
, filter
…).
Si, par le plus grand des hasards, vous utilisez Vue.js (ou un autre
framework du même acabit, tel React), reduce
sera un allié de poids
dans la définition de vos computeds, notamment — et je ne parle même pas de
Redux et de ses reducers, des noms qui devraient normalement commencer à
vous rappeler quelque chose ;)
Je vais vous laisser bricoler avec tout ça, et vous donner rendez-vous très prochainement pour un nouvel article !
L’équipe Synbioz.
Libres d’être ensemble.
Nos conseils et ressources pour vos développements produit.