Inicio / Artículos / Métodos modernos de arrays en JavaScript que reemplazan patrones genéricos comunes

Métodos modernos de arrays en JavaScript que reemplazan patrones genéricos comunes

Aprenda cómo los métodos más recientes de arrays en JavaScript, como groupBy, toSorted, with y findLast, reemplazan a los soluciones engorrosas de reduce, spread y reverse.

1432 palabras

Grupar datos sin una biblioteca: Object.groupBy() y Map.groupBy()

Durante años, para dividir un array de elementos en categorías según una propiedad común era necesario escribir el propio acumulador reduce() o utilizar lodash.groupBy solo para esa tarea.

La especificación ECMAScript ahora ofrece una función de agrupamiento nativa a través de Object.groupBy() y Map.groupBy().

Problema que resuelve

Cuando se obtiene una colección plana de registros desde una API —transacciones, tareas, entradas de usuarios, lo que sea— a menudo es necesario clasificarlos en grupos antes de mostrarlos o aplicar lógica empresarial sobre ellos.

Antes: implementarlo manualmente con 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;
}, {});

Después: una llamada clara y específica a 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 }
  ]
}
*/

Utilizar Map.groupBy() en su lugar

Cuando la clave de agrupamiento no es una cadena o símbolo simple, sino un objeto más complejo o una referencia dinámica, Map.groupBy() es la opción más adecuada:

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

Transformaciones inmutables en las que se puede confiar: toSorted(), toReversed(), toSpliced()

Un problema clásico de JavaScript es que los métodos familiares .sort(), .reverse() y .splice() reescriben el array en el mismo lugar en lugar de devolver uno nuevo.

En frameworks basados en un estado predecible — React, Redux, Vue y similares — modificar directamente un array tiende a generar errores sutiles y un rendering que silenciosamente deja de estar sincronizado.

Problema que resuelve

Para evitar estas trampas, los desarrolladores solían escribir primero copias defensivas, como [...array].sort() o array.slice().reverse(). La nueva familia de métodos “cambiar el array mediante copia” elimina por completo ese código genérico.

Comparación de los dos enfoques

// --- 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

Sustituir splice() por toSpliced()

toSpliced() permite eliminar, insertar o reemplazar elementos en cualquier posición sin alterar el array original:

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']

Actualizar un elemento de forma inmutable: with()

Supongamos que deseas cambiar el valor en un índice específico sin tocar el array de origen. El patrón habitual consiste en expandir el array en una copia, sobrescribir el índice y devolver el resultado. with() agrega todo esto en una sola expresión clara.

Problema que soluciona

Las actualizaciones de estado suelen requerir reemplazar un único elemento por posición manteniendo el arreglo circundante inmutable.

Comparación de los dos enfoques

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() también admite índices negativos, por lo que reemplazar un elemento contado desde el final es igual de sencillo:

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']

Búsqueda hacia atrás de forma eficiente: findLast() y findLastIndex()

Localizar un elemento con .find() funciona bien al escanear desde el principio, pero siempre comienza en el índice 0. Cuando lo que realmente se necesita es la última coincidencia, la solución habitual era invertir el arreglo primero; un enfoque que desperdicia tiempo y corre el riesgo de modificar datos que no se pretendía tocar.

Problema que soluciona

Los datos como los registros de logs, el historial de deshacer, las actualizaciones de actividad y los registros de transacciones están naturalmente ordenados por tiempo, y a menudo se desea obtener la entrada más reciente que cumpla con cierta condición.

Comparación de los dos enfoques

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

Dado que este método escanea desde el final y se detiene en cuanto encuentra una coincidencia, funciona en O(k) tiempo, donde k es la cantidad de pasos necesarios para llegar a la coincidencia; no hay necesidad de copiar o invertir todo el array primero.

5. Usar Map y Filter juntos: flatMap()

Es común encadenar .map() con .filter() o .flat() cuando cada elemento de origen puede generar cero, uno o varios resultados. flatMap() reduce esa cadena a una sola pasada.

Qué soluciona: evita la creación de arrays intermedios temporales cuando es necesario darle una nueva forma a datos anidados o filtrar y transformar en el mismo paso.

Ejemplo: aplanar una lista anidada de relaciones

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']

Ejemplo: transformar y filtrar al mismo tiempo

Puedes descartar un elemento devolviendo un array vacío y conservarlo devolviendo un array con un único elemento:

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. Acceso más limpio a elementos: at()

Obtener el último elemento de un array solía significar escribir array[array.length - 1], lo cual es incómodo y fácil de error.

at() toma prestada la convención de índices negativos familiar de Python, permitiéndote contar hacia atrás desde el final tanto de arrays como de cadenas.

Antes y despué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'

El resultado es más corto, menos propenso a errores de tipo “off-by-one” en las expresiones calculadas, y funciona bien con llamadas encadenadas a métodos ya que no es necesario almacenar el array en una variable temporal primero.

Cuándo utilizarlo

Lista de verificación para modernización

  • Revisar cómo se gestiona el estado: sustituir .sort() y .reverse() por .toSorted() y .toReversed() para evitar errores de mutación silenciosa.
  • Reducir el código genérico: utilizar Object.groupBy() en lugar de objetos acumuladores creados manualmente.
  • Acelerar las búsquedas: reemplazar los patrones de buscar primero y luego invertir el orden por findLast() para datos ordenados por tiempo.
  • Simplifique la sintaxis cotidiana: utilice .at(-1) y .with(index, value) en lugar de realizar cálculos manuales basados en la longitud y copias defensivas de arrays.
  • Lecturas relacionadas