Array JS : 7 fonctions à connaître
Dans cet article nous allons prendre le temps de regarder comment manipuler les array ou les tableaux en JavaScript. On va faire ensemble du "data-mining", de l’exploration de données. Et à la fin vous serez … un ninja des tableaux.
Il faut savoir que JavaScript fournit sur les Array (les tableaux) des fonctions qui permettent de faire les choses de manière plus propre et plus lisibles que si on les fait à la main. Donc c’est dans notre intérêt – et dans l’intérêt de notre code – de bien comprendre et utiliser ces fonctions.
Je ne vais pas faire le tour de toutes les fonctions qui peuvent s’appliquer aux array, juste ceux qui me sont utiles régulièrement.
Setup
On va travaille tout le long avec un tableau qui contient des objects qui représentent des personnages de Star Wars. Cet array est pris du « SWAPI » ce qui signifie Star Wars API. C’est une api classique en REST dont je suis allé chercher quelques données, que j’ai copiées en json.
Pour vous simplifier la vie et vous aider à suivre, j’ai rajouté cet array au module NPM @kodaps/faker-module
que j’ai créé dans un article précédent. Pour suivre il vous suffit d’importer et de lire ce module. Pour faire ça, il suffit de créer un dossier, d’y aller, puis de faire yarn init
(ou npm init
) pour initialiser un projet.
Ensuite on répond aux diverses questions puis on fait :
yarn add @kodaps/faker-module
ou
npm install @kodaps/faker-module --save`
A présent je lance visual code dans ce dossier en faisant code .
. Je crée un index.js
et je commence par importer swapi
du module et je fais un console log
de la longueur de l’array en faisant swapi.length
.
Ensuite je lance Node JS (avec F5 dans visual studio code) et là il râle parce que je fais un import, et que je me mets donc dans le contexte d’un module. Il faut donc rajouter dans le package.json :
"type" : "module"
Première manipulations & explorations
A présent, je lance le fichier JS avec le débuggeur de Visual Studio code, et le console log me retourne un chiffre. Alors, ici, pour moi, en l’état actuel des choses, ce chiffre c’est 30. Mais si lisez ceci dans un avenir suffisamment lointain il est possible qu’il soit plus grand pour vous, si je rajoute des données d’ici là.
On a donc commencé la première manipulation de l’array, avec le « length » qui nous donne la longueur.
A présent on va commencer par la base de la base, on va faire une boucle bête et méchante pour voir la tête qu’ont les éléments du tableau, et commencer notre exploration des données, notre data mining.
On va faire un boucle sur chaque élément du tableau avec for
, et on va récupérer chaque élément :
for (let i=0; i < swapi.length; i++) {
let item = swapi[i];
console.log(item);
}
Voilà, le code nous imprime l’ensemble des éléments dans notre tableau. Alors ça c’est la façon « old school » de faire les boucles, et vous vous doutez bien qu’on ne va pas s’arrêter là. Parce que le javascript propose une syntaxe plus resserrée, plus concentrée, mais surtout que je trouve beaucoup plus lisible :
swapi.forEach(function (a) {
console.log(a);
})
`
On peut faire encore plus condensé, avec les fonctions flèche.
swapi.forEach((a) => console.log(a));
Il y a une seule ligne donc pas besoin de crochet. On lance, et on obtient le même résultat. Est-ce que vous saviez qu’on peut l’écrire de manière encore plus condensée ? Je vous laisse y réfléchir et je vous montrerai comment à la fin. A présent on va regarder un des éléments du tableau, et pour ça on va utiliser la fonction find
.
La fonction Array.find
Comme forEach
, la fonction find prend en paramètre une fonction, ici cette fonction en paramètre analyse l’élément qu’il reçoit en paramètre pour nous dire si oui ou non on a trouvé un bon élément. La fonction find
envoie le premier élément pour lequel la fonction en paramètre renvoie true
. Du coup voyons si nous avons Yoda dans notre tableau:
let yoda = swapi.find(function(items) {
return item.name == "Yoda";
});
Il faut se méfier de la fonction find
il faut se méfier puisqu’il peut ne rien trouver et donc renvoyer undefined. Maintenant regardons les valeurs de l'objet représentant Yoda. Si on examine ce que nous dit la console, on voit que la taille (le « height ») et la masse sont en chaine de caractère et pas en nombre, puisqu’il y a des guillemets autour.
Pourquoi ça ? ET bien si on regarde le console log général, on voir que pour certains personnages, ces données ne sont pas connues, donc il y a des « unknown ». Du coup, je propose qu’on enlève les valeurs inconnues du tableau, pour pouvoir traduire ensuite pour ceux qui restent la masse et la taille en valeurs numériques. Comment retire-t-on ces éléments ? Et bien c’est l’occasion de voir une nouvelle fonction : la fonction filter
.
Abonnez-vous pour mieux comprendre le développement logiciel. Recevez les dernière nouvelles, vidéos et conseils.
La fonction Array.filter
Avec cette fonction, on va filtrer pour enlever les valeurs « unknown ». L’idée de la fonction filter, c’est qu’elle prend en paramètre une fonction. D’ailleurs la plupart de ces fonctions de manipulations de tableau prennent en paramètre une fonction, qui va ensuite s’appliquer un à un à l’ensemble des éléments du tableau. Celle-ci en paramètre prend en paramètre un ou des éléments du tableau.
Ici la fonction qu’exécute filter prend un élément du tableau en paramètre et renvoie une booléenne. Et cette fonction répond à la question : est-ce qu’on garde cet élément dans le nouvel tableau que nous sommes en train de construire ?
Si la fonction analyse l’élément et renvoie une valeur vraie, on garde l’item dans le nouvel tableau, si non.. cet élément est retiré.
let noUnknowns = swapi.filter(function(a) {
return a.height != 'unknwown' && a.mass != 'unknown';
});
Une fois de plus il y a une façon plus concise d’écrire la même chose avec la syntaxe des grosses flèches.
let noUnknowns = swapi.filter((a) => a.height != 'unknwown' && a.mass != 'unknown');
Comme ce n’est qu’une seule ligne, le return
peut être sous entendu. Du coup si à présent on récupère la longueur du nouvel array, avec noUnknowns point length, on obtient 27.
Maintenant on voudrait transformer toutes ces valeurs textuelles en valeur numériques. Pour ça on va utiliser la fonction parseFloat qui essaie de transformer une chaine de texte en flottant. Du coup on l’idée c’est de parcourir chaque élément du tableau, et de fait l’opération sur chaque élément.
La fonction Array.map
Il y a une fonction qui fait exactement ce qu’on veut, qui passe sur chaque élément, le modifie, et renvoie un nouveau tableau. Cette fonction s’appelle « map », puisqu’à chaque élément elle associe un nouvel élément modifié. Cette fois-ci on va faire les choses légèrement différemment. On va écrire la fonction de traitement à part, et on va lire la taille et la masse de nos personnages et les convertir en flottant.
function updateChar(character) {
// on lit la taille
let height = parseFloat(character.height) ;
// on lit la masse
let mass = parseFloat(character.mass) ;
}
Et puisqu’on est là, on va calculer l’indice de masse corporelle, ou IMC (ou « Bodily Mass Index » et BMI en anglais). L’IMC c’est la masse en kilogrammes divisée par la taille en mètres. Ici dans notre cas la taille est en centimètres donc on va devoir diviser par 100. Ca nous donne donc :
function updateChar(character) {
// on lit la taille
let height = parseFloat(character.height) ;
// on lit la masse
let mass = parseFloat(character.mass) ;
// on calcule l'IMC
let bmi = mass / (height/100)**2;
}
Ensuite on revoie un nouvel élément qu’on crée à la volée, et pour ça on va utiliser la déstructuration (...character
), qui a un double avantage: ça s’écrit facilement, et surtout, en écrivant comme ça on est certains d’avoir tous les paramètres, et on ne modifie pas le paramètre character
en entrée, qui est un objet.
function updateChar(character) {
// on lit la taille
let height = parseFloat(character.height) ;
// on lit la masse
let mass = parseFloat(character.mass) ;
// on calcule l'IMC
let bmi = mass / (height/100)**2;
return { …character, mass, height, bmi}
}
Alors c’est quoi l’avantage de ne pas modifier le character en entrée ? Si on l’avait modifié, ça aurait modifié le contenu du tableau d’origine, ce qui crée un effet de bord indésirable. Maintenant qu’on a crée notre fonction, appliquons-la à notre tableau en faisant :
Let floatValues=noUnknowns.map(updateChar);
floatValues.forEach((a) => console.log(a));
Et on voit qu’on a bien des valeurs numériques dans height et mass. Le “bmi” est à la fin parce qu’on l’a ajouté, il ne vient pas remplacer une valeur existante.
A présent, nous allons calculer la hauteur moyenne de nos personnages. Pour calculer la moyenne, il faut faire la somme de toutes les hauteurs, puis de diviser par le nombre d’éléments dans le tableau. Mais comment faire cette somme des hauteurs ? La manière classique serait de faire une boucle, mais ça c’est pour les petits joueurs. Vous vous en doutez, il y a une fonction des tableaux qui permet de faire justement ce qu’on veut.
Le fonction Array.Reduce
Cette fonction s’appelle reduce
. Elle prend en paramètre une fonction (à priori si vous avez suivi jusqu’ici ce n’est pas une surprise). Mais cette fonction est un peu différente, puisqu’elle prend en paramètre deux valeurs.
- La première, c’est la valeur accumulée courante. En gros, dans notre cas c’est la somme en cours.
- Et le deuxième paramètre, de manière plus classique, c’est l’élément.
function addHeight(currentTotal, item) {
return currentTotal + item.height;
}
Ensuite on appelle reduce sur notre tableau:
let totalHeight = floatValues.reduce(addHeight, 0);
Donc Ici on a passé en paramètre notre fonction de réduction, et on a ajouté un deuxième paramètre. Ce paramètre c’est tout simplement le point de départ, la valeur initiale de pour notre valeur accumulée. Au passage il faut savoir que si on ne met pas ce paramètre, reduce
va supposer que la valeur initiale c’est le premier élément du tableau. Dans notre cas à nous, ça nous va pas du tout puisque le premier élément de notre tableau c’est un objet, pas un chiffre.
En gros, à moins d’avoir un tableau de nombres, il faut toujours passer une valeur initiale en deuxième paramètre. Ici on a mis 0 en valeur initiale parce qu’on fait une addition, mais si on faisait une multiplication on aurait mis 1, dans d’autres contextes ça pourrait imaginer avoir un tableau ou une chaine vides.
Bref, la valeur initiale dépend de ce que vous être en train de faire comme réduction accumulation. Dans tous les cas, on a donc notre somme des hauteurs, on divise par la longeur pour voir le résultat, et donc on fait :
console.log(totalHeight/floatValues.length);
Donc ça nous donne une taille moyenne de 165, 7 soit un mètre soixante-six environ. Maintenant on va regarder la masse de nos personnages. On va commencer par trier nos personnages, du plus léger au plus lourd. Sans grande surprise, il y a une fonction des tableaux qui permet de faire ça.
La fonction Array.Sort
De manière pas particulièrement originale, cette fonction s’appelle « sort ». Alors avant d’aller plus loin, je veux vous montrer un petit piège dans cette fonction. Pour illustrer ça on va appeler sort sur le tableau:
const arr = [1, 5, 10, 20, 75, 100]
Du coup, dans notre code en cours, on fait:
console.log([1, 5, 10, 20, 75, 100].sort)
Avant que j’exécute, est-ce que vous arrivez à deviner ce que ça va donner ? Bon, ce n’est pas drôle, je vous ai prévenus qu’il y avait un piège. Si on exécute on voit que ça fait
[1, 10, 100, 5, 75]
Pourquoi ? Si on appelle sort sans paramètre, ça renvoie un tri en ayant converti chaque élément en chaine de caractères au préalable. Autrement dit, à moins que vous ne soyez en train de trier des string, si vous ne voulez pas de mauvaise surprise, passez une fonction en paramètre.
Alors justement, quelle tête a le fonction qu’on passe en paramètre ?
Elle prend deux paramètres, qu’on va appeler a et b dans notre cas, et qui représentent deux éléments adjacents dans le tableau. En gros la question que la question sort se post c’est est-ce que je dois, dans le tableau, échanger ces deux éléments que je reçois en paramètre. Et en fonction de si le chiffre renvoyé est négatif ou non, sort échange les paramètres.
Le moyen mnémotechnique que j’utilise personnellement, c’est comme la fonction retourne un chiffre, si ce chiffre est négatif, et bien non, négatif, pas besoin de les échanger, a reste bien avant b. Et si le chiffre est positif, alors oui, il faut placer b avant a. Du coup pour revenir à notre exemple de tableau, si je veux que ce soit classé par ordre croissant, je dois faire
[1, 10, 100, 5, 75].sort((a,b) => a – b) ;
Puisque si a est plus petit que b, alors on veut qu’il soit placé en premier dans la liste, donc on veut garder l’ordre, donc négatif, on ne veut pas changer. Et si a est plus petit que b alors a – b est donc négatif. On va appliquer un peu la même chose sur notre tableau, sauf qu’on veut trier par poids. Enfin par masse. Puisque je vous rappelle que c’était ça le but à la base !
Mais du coup après toutes ces divagations, l’application à notre cas à nous va être relativement trivial, puisque pour trier notre tableau par masse croissantes il suffit de faire :
```javascript
const sorted = floatValues.sort((a,b) => a.mass – b.mass);
Et maintenant, si on veut la valeur du milieu, celle qui a autant d’éléments de chaque côté, on regarde la longueur du tableau et on divise par 2. Ici mon tableau fait 27, et 27/2 = 13.5, donc si on va chercher quelle est le personnage qui se trouve à cet endroit, en faisant
console.log(sorted[13])
On voit que la valeur médiane de la masse, celle où il en a autant de chaque côté, c’est 77kg.
Les fonctions Array.any et Array.every
Pour finir on va voir deux fonctions assez simples, qui s’appelle « any » et « every ». L’idée c’est que ces fonctions prennent, elles aussi, une fonction en paramètre, et que cette fonction renvoie true ou false.
Donc par exemple si on regarde le champ birth_year
(année de naissance) sur notre ami Yoda, on voit que c’est “896BBY”.
Si on prend notre valeur médiane d’un peu plus haut, qui était Wedge Antilles, on voit que son birth_year
est 21BBY. La question qui pourrait se poser c’est : est-ce que toutes les dates suivent se format avec BBY ?
On a la fonction endsWith
qu’on peut appliquer sur une chaine de caractère pour nous dire si le champ texte se termine bien avec ce qu’on lui passe en paramètre:
yoda.birth_year.endsWith(‘BBY’) // true
Est-ce que c’est le cas pour chaque personnage ? Pour ça on va utiliser la fonction every
. On va commencer par faire une fonction « endsWithBB » qu’on va écrire de manière condensée:
const endsWithBB = (a) => a.birth_year.endsWith('BBY');
Puis nous allons "logger" le résultat de l’appel à every
, qui va regarder si chaque element renvoie true (en gros, il fait un gros AND logique entre les valeurs renvoyées)
console.log(sorted.every(endsWithBB));
Ca renvoie false
, donc il y a bien des dates qui ne finissent pas en BBY.
La fonction any
a un fonctionnement analogue. La fonction every
répond à la question de « est-ce que tous les éléments répondent à ce critère » La fonction any
, elle, répond à la question : « est-ce qu’il existe des éléments qui répondent à ce critère ». (en gros, il fait un gros OU logique entre les valeurs renvoyées). Par exemple on a des personnages male, femelle ou « n/a » (non applicable) pour les droids, mais est-ce qu’il existe des personnages ou le champ gender ne rentre dans aucune de ces trois catégories ? Je vous laisse expérimenter.
Le chainage de fonction Array
Un autre chose importante à dire : parmi ces fonctions, un certain nombre (comme filter
, ou map
ou sort
) renvoient eux-mêmes un tableau. Du coup c’est possible de chainer les instructions, en faisant un point filter puis un point sort. Donc par exemple on aurait pu faire un filter des unknowns, puis un map pour convertir les chaines en texte, puis un sort pour trier par masse:
swapi.filter(isKnown).map(toFloat).sort((a,b) => a.mass – b.mass);
Avant de revenir sur la solution plus courte pour le console log, on a donc vu :
• find
pour trouver le premier élément qui répond à un critère
• filter
pour filtrer les valeurs qui suivent un critière
• forEach
pour faire une boucle sur le tableau
• map
pour changer des valeurs
• sort
pour trier le tableau
• any
pour vérifier s’il existe un élément qui répond à un critère
• every
pour vérifier si tous les éléments répondent à un critère
Avant de vous quitter, je vous avais promis une façon plus concise d’écrire
swapi.forEach((a) flèche console.log(a))
Est-ce que vous l’avez trouvée ?
En réalité, on peut faire simplement :
swapi.forEach(console.log),
la fonction qu’on passe en paramètre ne fait rien de plus qu’un console log