JavaScript, les bons éléments, façon 2020 : Des tableaux boostés

On vient juste de parler des itérateurs, il est donc temps d’enchaîner avec la prochaine partie qui reste dans le domaine des tableaux : l’ensemble des nouvelles méthodes qui leur sont ajoutés.

Pendant assez longtemps, manipuler des tableaux, c’est à dire trouver/copier/réduire dans un tableau se limitait à une boucle for et, de manière optionnelle, un appel à .slice() pour copier le tableau.

Ce temps là est révolu ! Bienvenue dans le JavaScript qui embrasse ses fonctions lambdas, le JavaScript qui puise sa force de son côté fonctionnel.

On a déjà évoqué la "nouvelle" méthode .forEach() (2009 quand même), mais elle n’est pas seule : Un set de méthodes très puissantes ont été progressivement ajoutées à JS ces 12 dernières années.

Rechercher et valider

La partie la plus importante de ces nouvelles méthodes se concentrent sur la recherche d’éléments, que ça soit pour les obtenir ou à des fins de validation (pour s’assurer que le tableau ne contienne pas un élément donné, par exemple).

every et some

Ces deux méthodes sont conçues pour valider le contenu d’un tableau de deux manières différentes : .every(callback) renverra true si le callback invoqué pour chaque élément renvoie true, sinon il renverra false.

Au contraire, .some(callback) renverra true au premier élément dont le callback renvoie true. Si tous renvoient des valeurs équivalentes à false, alors some renvoie false à son tour.

const arr = [1, 2, 30, 50];

arr.some(function (element) {
  return element >= 40;
}); // true : un élément valide la condition, 50 >= 40

// .some est équivalent à la fonction suivante:
function arraySome(array, callback) {
  for (const element of array) {
    if (callback(element)) {
      return true;
    }
  }

  return false;
}

arr.every(function (element) {
  return element >= 40;
}); // false : un élément échoue la condition, 1 < 40

// .every est équivalent à la fonction suivante:
function arrayEvery(array, callback) {
  for (const element of array) {
    if (!callback(element)) {
      return false;
    }
  }
  
  return true;
}

find et findIndex

Ici, on a deux méthodes prenant un callback en paramètre (une fonction de rappel). Cette fonction, qui recevra l’élément en cours en paramètre, pourra renvoyer une valeur équivalente à true si l’élément actuel est l’élément recherché. Si rien (ou une valeur équivalente à false) n’est renvoyé, alors la recherche continue.

  • .find(callback) renvoie l’élément ayant validé le callback. Si aucun élément n’a validé, elle renvoie undefined.
  • .findIndex(callback) renvoie l’index de l’élément ayant validé le callback. Si aucun élément n’a validé, elle renvoie -1.
const arr = [1, 2, 30, 50];

arr.find(function (element) {
  return element >= 25;
}); // 30 ; C'est le premier élément (dans l'ordre) qui valide la condition

arr.find(function (element) {
  return element < 0;
}); // undefined ; Aucun élément n'a validé la closure

// .find est équivalent à cette fonction :
function arrayFind(array, callback) {
  for (const element of array) {
    if (callback(element)) {
      return element;
    } 
  }
}

// .findIndex fait pareil, mais renvoie l'index
arr.findIndex(function (element) {
  return element >= 25;
}); // 2

arr.findIndex(function (element) {
  return element < 0;
}); // -1 ; Aucun élément n'a validé la closure

// .findIndex est équivalent à cette fonction :
function arrayFindIndex(array, callback) {
  for (let i = 0; i < array.length; i++) {
    if (callback(array[i])) {
      return i;
    }
  }

  return -1;
}

includes et indexOf

Celles-ci ont plus ou moins la même utilité que les deux précédentes, à quelques choses près :

  • Elles ne prennent pas de fonction de rappel, mais l’élément à chercher
  • .includes(element) renvoie un booléen ; si l’élément existe ou non
  • .indexOf(element) renvoie l’index de l’élément demandé ; -1 si il n’existe pas

Ne nécessitant pas de lancer de fonction à chaque itération, elles sont plus rapides que leurs contreparties .find() et .findIndex().

const arr = [1, 2, 30, 50];

arr.includes(3); // false
arr.includes(30); // true

arr.indexOf(3); // -1
arr.indexOf(30); // 2

// .includes peut être polyfillée par ceci
function arrayIncludes(array, element) {
  for (const current of array) {
    if (element === current) {
      return true;
    }
  }

  return false;
}

// .indexOf peut être polyfillée par ceci
function arrayIndexOf(array, element) {
  for (let i = 0; i < array.length; i++) {
    if (array[i] === element) {
      return i;
    }
  }

  return -1;
}

Faites cependant attention avec ces méthodes si vos tableaux ne stockent pas des primitives : En JavaScript, les objets sont comparés par référence et non pas par valeur.

Transformer

Les trois fonctions majeures (en tout cas, les plus utilisées) de JavaScript sont ici. Le but de ces trois méthodes est de prendre le tableau original et d’en renvoyer une forme modifiée.

  • .map(callback) renverra un tableau de même taille, avec pour valeur le retour du callback invoqué sur chaque élément
  • .filter(callback) renverra un tableau contenant les éléments où callback a renvoyé une valeur équivalente à true
  • .reduce(callback, initial_value) utilisera callback pour convertir le tableau en une seule valeur

Ces trois méthodes sont réellements des bases de la programmation fonctionnelle avec les tableaux en JavaScript, leur utilisation est extrêmement courante, il convient donc de les maîtriser.

Nous allons voir trois exemples très basiques liés à ces fonctions.

const arr = [1, 2, 30, 50];

// On veut doubler chaque nombre du tableau
arr.map(function (element) {
  return element * 2;
}); // [2, 4, 60, 100] ; Chaque élément a été multiplié par 2


// On veut filtrer les nombres supérieurs à 20
arr.filter(function (element) {
  return element <= 20;
}); // [1, 2] ; 30 et 50 ne validaient pas la condition


// On veut une somme des nombres du tableau
arr.reduce(function (accumulator, current_element) {
  // Au premier appel, {accumulator} vaut {initial_value}
  // Ici, {initial_value} est 0 (deuxième paramètre de reduce)

  // Au prochain appel, {accumulator} vaudra le retour de l'appel en cours
  // Ainsi, l'appel n+1 recevra le retour de l'appel n dans la variable {accumulator}

  // {current_element} représente l'élément actuel itéré dans le tableau

  return accumulator + current_element;
}, 0); // 83


// On peut construire les polyfills suivant qui expliquent leur fonctionnement :
function arrayMap(array, callback) {
  // Crée un tableau de taille identique à array
  const copy = Array(array.length);

  for (let i = 0; i < array.length; i++) {
    copy[i] = callback(array[i]);
  }

  return copy;
}

function arrayFilter(array, callback) {
  const copy = [];

  for (const element of array) {
    if (callback(element)) {
      copy.push(element);
    }
  }

  return copy;
}

function arrayReduce(array, callback, initial_or_current_value) {
  if (!array.length) {
    return initial_or_current_value;
  }

  let i = 0;
  if (initial_or_current_value === undefined) {
    // La valeur initiale est facultative, dans ce cas elle
    // vaut le premier élément du tableau (et on commence à 1)
    initial_or_current_value = array[0];
    i = 1;
  }

  for (; i < array.length; i++) {
    initial_or_current_value = callback(initial_or_current_value, array[i]);
  }

  return initial_or_current_value;
}

Modifier

Enfin, c’est le plus petit set de méthodes : celles-ci, toutes deux récentes, servent à modifier le contenu ou l’organisation d’un tableau.

  • .fill(valeur, début?, fin?) permet de remplacer le contenu ou une plage du contenu d’un tableau par une valeur précise. Cette méthode agit sur le tableau en cours, elle ne crée pas de copie
  • .flat(profondeur?) permet "d’aplatir" un tableau, c’est à dire supprimer les éventuels niveaux de profondeur à l’intérieur de celui-ci
[1, 2, 30, 50].fill(30); // [30, 30, 30, 30]
[1, 2, 30, 50].fill(30, 1); // [1, 30, 30, 30]
[1, 2, 30, 50].fill(25, 1, 3); // [1, 25, 25, 50]

// Un tableau avec différents niveaux de profondeur
const arr = [ 
  [ 
    [
      1, 
      2
    ], 
    3 
  ], 
  [
    4, 
    5
  ], 
  [ 
    6,
    [ 
      [
        7, 
        8
      ] 
    ]
  ],
  9 
];

// équivalent à arr.flat(1);
arr.flat(); // [[1, 2], 3, 4, 5, 6, [[7, 8]], 9]
arr.flat(3); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

// On peut écrire une équivalence à .flat
function arrayFlat(array, profondeur) {
  if (profondeur === undefined) {
    profondeur = 1;
  }
  if (profondeur < 0) {
    return [];
  }

  return arrayFlat.deepFlat([], array, profondeur);
}

arrayFlat.deepFlat = function (copy, nested_array, profondeur) {
  for (const item of nested_array) {
    if (item instanceof Array && profondeur > 0) {
      // Ajoute tous les éléments de item dans copy
      this.deepFlat(copy, item, profondeur - 1);
    }
    else {
      copy.push(item);
    }
  }

  return copy;
};

La fin

Cet article est très court, je vous laisse donc enchaîner avec le suivant, qui parle des améliorations au niveau des fonctions : fonctions varidiques, paramètres par défaut, fonctions fléchées !

À bientôt 😀 !

Article précédent : Générateurs et itérateursArticle suivant : La function update

Laisser un commentaire