Accueil / Articles / Méthodes d’array modernes de JavaScript qui remplacent les schémas de base courants

Méthodes d’array modernes de JavaScript qui remplacent les schémas de base courants

Découvrez comment de nouvelles méthodes des tableaux JavaScript telles que groupBy, toSorted, with et findLast remplacent les solutions complexes basées sur reduce, spread et reverse.

1432 mots

Grouper des données sans bibliothèque : Object.groupBy() et Map.groupBy()

Pendant des années, pour diviser un tableau d’éléments en catégories en fonction d’une propriété commune, il fallait soit écrire son propre accumulateur reduce(), soit utiliser lodash.groupBy uniquement pour cette tâche.

La spécification ECMAScript propose désormais un mécanisme de groupe natif grâce à Object.groupBy() et Map.groupBy().

Problème résolu

Lorsque vous récupérez une collection de données brutes depuis une API — transactions, tâches, entrées d’utilisateurs, etc. — vous avez souvent besoin de les classer par catégories avant de les afficher ou d’y appliquer une logique métier.

Auparavant : implémentation manuelle avec reduce()

const inventory = [
  { name: 'Apples', type: 'fruit', quantity: 10 },
  { name: 'Bananas', type: 'fruit', quantity: 0 },
  { name: 'Carrots', type: 'vegetable', quantity: 14 },
  { name: 'Broccoli', type: 'vegetable', quantity: 5 }
];

const groupedByReduce = inventory.reduce((acc, item) => {
  const key = item.type;
  if (!acc[key]) {
    acc[key] = [];
  }
  acc[key].push(item);
  return acc;
}, {});

Après : appel clair et adapté à Object.groupBy

const inventory = [
  { name: 'Apples', type: 'fruit', quantity: 10 },
  { name: 'Bananas', type: 'fruit', quantity: 0 },
  { name: 'Carrots', type: 'vegetable', quantity: 14 },
  { name: 'Broccoli', type: 'vegetable', quantity: 5 }
];

const grouped = Object.groupBy(inventory, item => item.type);

/*
Output:
{
  fruit: [
    { name: 'Apples', type: 'fruit', quantity: 10 },
    { name: 'Bananas', type: 'fruit', quantity: 0 }
  ],
  vegetable: [
    { name: 'Carrots', type: 'vegetable', quantity: 14 },
    { name: 'Broccoli', type: 'vegetable', quantity: 5 }
  ]
}
*/

Préférer Map.groupBy()

Lorsque la clé de regroupement n’est pas une simple chaîne ou un symbole, mais plutôt un objet plus complexe ou une référence dynamique, Map.groupBy() est la solution plus adaptée :

const vipTier = { tier: 'VIP' };
const standardTier = { tier: 'Standard' };

const customers = [
  { name: 'Alice', plan: vipTier },
  { name: 'Bob', plan: standardTier },
  { name: 'Charlie', plan: vipTier }
];

const groupedByPlan = Map.groupBy(customers, user => user.plan);

console.log(groupedByPlan.get(vipTier));
// Returns Alice and Charlie's records directly keyed by reference

Transformations immuables fiables : toSorted(), toReversed(), toSpliced()

Un piège classique en JavaScript est le fait que les méthodes familières .sort(), .reverse() et .splice() modifient l’array en place au lieu de retourner un nouvel array.

Dans les frameworks basés sur un état prévisible — React, Redux, Vue et similaires — modifier directement un array entraîne souvent des bugs subtils ainsi qu’un affichage qui cesse silencieusement d’être synchronisé.

Problème résolu

Afin d’éviter ces pièges, les développeurs avaient l’habitude d’écrire d’abord des copies de protection, comme [...array].sort() ou array.slice().reverse(). La nouvelle famille de méthodes « modifier le tableau par copie » élimine complètement ce code générique.

Comparaison des deux approches

// --- The Old Mutating Way ---
const scores = [88, 92, 79, 95];
const sortedMutated = scores.sort((a, b) => a - b);

console.log(scores); // [79, 88, 92, 95] -> Original data was destroyed!

// --- The Modern Non-Mutating Way ---
const originalScores = [88, 92, 79, 95];
const cleanSorted = originalScores.toSorted((a, b) => a - b);

console.log(originalScores); // [88, 92, 79, 95] -> Unchanged
console.log(cleanSorted);    // [79, 88, 92, 95] -> Fresh array

Remplacer splice() par toSpliced()

toSpliced() vous permet de supprimer, d’insérer ou de remplacer des éléments à n’importe quelle position sans toucher au tableau d’origine :

const tabs = ['Home', 'About', 'Pricing', 'Contact'];

// Remove 'Pricing' (index 2) and insert 'Services' & 'Blog'
const updatedTabs = tabs.toSpliced(2, 1, 'Services', 'Blog');

console.log(tabs);        // ['Home', 'About', 'Pricing', 'Contact']
console.log(updatedTabs); // ['Home', 'About', 'Services', 'Blog', 'Contact']

Mettre à jour un élément de manière immuable : with()

Supposons que vous souhaitiez modifier la valeur à un index donné sans affecter le tableau source. La méthode habituelle consiste à étendre le tableau en une copie, à écrire sur l’index concerné et à retourner le résultat. with() regroupe tout cela en une seule expression claire.

Problème résolu

Les mises à jour des états nécessitent souvent de remplacer un seul élément en fonction de sa position tout en conservant le tableau environnant inchangé.

Comparaison des deux approches

const months = ['Jan', 'Mar', 'Mar', 'Apr'];

// Old way: Spread and mutate copy
const fixedMonthsOld = [...months];
fixedMonthsOld[1] = 'Feb';

// Modern way: array.with(index, value)
const fixedMonthsNew = months.with(1, 'Feb');

console.log(months);         // ['Jan', 'Mar', 'Mar', 'Apr']
console.log(fixedMonthsNew);  // ['Jan', 'Feb', 'Mar', 'Apr']

with() prend également en charge les indices négatifs, ce qui rend le remplacement d’un élément compté à partir de la fin tout aussi simple :

const tags = ['v1.0.0', 'v1.1.0', 'v1.2.0-beta'];
const stableTags = tags.with(-1, 'v1.2.0');

console.log(stableTags); // ['v1.0.0', 'v1.1.0', 'v1.2.0']

Recherche efficace vers l’arrière : findLast() et findLastIndex()

L’utilisation de .find() pour localiser un élément fonctionne bien lorsqu’on parcourt le tableau depuis le début, mais elle commence toujours à l’index 0. Lorsqu’il s’agit en réalité de trouver la dernière occurrence, la solution habituelle consiste à inverser d’abord le tableau — une méthode qui gaspille du temps et risque de modifier des données auxquelles on ne voulait pas toucher.

Problème résolu

Les données telles que les entrées de journal, l’historique des actions annulées, les flux d’activité et les enregistrements de transactions sont naturellement ordonnées par date, et on souhaite souvent obtenir la dernière entrée correspondant à une certaine condition.

Comparaison des deux approches

const logs = [
  { id: 1, action: 'LOGIN', timestamp: 1000 },
  { id: 2, action: 'UPLOAD', timestamp: 1010 },
  { id: 3, action: 'ERROR', timestamp: 1020 },
  { id: 4, action: 'UPLOAD', timestamp: 1030 },
  { id: 5, action: 'LOGOUT', timestamp: 1040 }
];

// Old way: Reversing creates overhead and array allocation
const lastUploadOld = [...logs].reverse().find(log => log.action === 'UPLOAD');

// Modern way: findLast iterates backwards directly
const lastUpload = logs.findLast(log => log.action === 'UPLOAD');
const lastUploadIdx = logs.findLastIndex(log => log.action === 'UPLOAD');

console.log(lastUpload);    // { id: 4, action: 'UPLOAD', timestamp: 1030 }
console.log(lastUploadIdx); // 3

Puisqu’il parcourt la liste à partir de la fin et s’arrête dès qu’il trouve une correspondance, il fonctionne en O(k) temps, où k est le nombre d’éléments à examiner pour trouver la correspondance — il n’est pas nécessaire de copier ou d’inverser tout le tableau au préalable.

5. Utiliser Map et Filter ensemble : flatMap()

Il est courant de chaîner .map() avec .filter() ou .flat() lorsque chaque élément source peut générer zéro, un ou plusieurs résultats. flatMap() regroupe cette chaîne en une seule passe.

Problème résolu : il évite la création d’arrays intermédiaires temporaires lorsque l’on doit reconfigurer des données imbriquées ou filtrer et transformer en même temps.

Exemple : aplatir une liste imbriquée de relations

const authors = [
  { name: 'Author A', books: ['Book 1', 'Book 2'] },
  { name: 'Author B', books: ['Book 3'] },
  { name: 'Author C', books: [] }
];

// Map + Flat produces 2 array allocations
const booksNested = authors.map(a => a.books).flat();

// flatMap runs the map and flattens depth-1 in one pass
const allBooks = authors.flatMap(a => a.books);
console.log(allBooks); // ['Book 1', 'Book 2', 'Book 3']

Exemple : transformer et filtrer en même temps

On peut éliminer un élément en renvoyant un array vide et le conserver en renvoyant un array à un seul élément :

const rawInputs = ['42', 'invalid', '100', 'undefined', '256'];

const validNumbers = rawInputs.flatMap(input => {
  const num = parseInt(input, 10);
  return Number.isNaN(num) ? [] : [num];
});

console.log(validNumbers); // [42, 100, 256]

6. Accès aux éléments plus simple : at()

Obtenir le dernier élément d’un array signifiait traditionnellement écrire array[array.length - 1], ce qui est peu pratique et sujet aux erreurs.

at() emprunte la convention des indices négatifs familière en Python, vous permettant de compter à rebours depuis la fin des arrays comme des chaînes de caractères.

Avant et après

const queue = ['Alice', 'Bob', 'Charlie', 'Dana'];

// Old way
const lastPersonOld = queue[queue.length - 1];
const secondLastOld = queue[queue.length - 2];

// Modern way
const lastPerson = queue.at(-1);    // 'Dana'
const secondLast = queue.at(-2);    // 'Charlie'

Le résultat est plus court, moins sujet aux erreurs de type « off-by-one » dans les expressions calculées, et il s’intègre bien aux appels de méthodes en chaîne puisque vous n’avez pas besoin de stocker le tableau dans une variable temporaire au préalable.

Quand l’utiliser

Liste de contrôle pour la modernisation

  • Vérifiez comment vous gérez l’état : remplacez .sort() et .reverse() par .toSorted() et .toReversed() pour éviter les bugs de mutation silencieuse.
  • Éliminez les réducteurs redondants : utilisez Object.groupBy() à la place des objets accumulateurs manuellement créés.
  • Accélérez les recherches : remplacez les schémas de recherche inversée par findLast() pour les données ordonnées dans le temps.
  • Simplifiez la syntaxe quotidienne : utilisez .at(-1) et .with(index, value) au lieu de calculs manuels basés sur la longueur et de copies défensives des tableaux.
  • Lectures complémentaires