Ordenar y buscar en arrays de JavaScript

Ordenar y buscar parece lo más fácil del mundo, pero sort() tiene la sorpresa más famosa de JavaScript: ordena los números como si fueran texto. Aquí está cómo hacerlo bien en cada caso.

Números: siempre con comparador

const numeros = [10, 1, 5, 100, 25];
console.log([...numeros].sort());
console.log([...numeros].sort((a, b) => a - b));
console.log([...numeros].sort((a, b) => b - a));
salida[ 1, 10, 100, 25, 5 ]
[ 1, 5, 10, 25, 100 ]
[ 100, 25, 10, 5, 1 ]

Sin argumentos, sort() convierte cada elemento en texto y los compara letra a letra, como en un diccionario. El comparador (a, b) => a - b devuelve un número negativo si a va antes, positivo si va después y cero si dan igual. Para el orden inverso, b - a.

sort sin comparador ordena los números como texto; con a – b los ordena como númerosPor qué sort() deja el 100 antes que el 25[10, 1, 5, 100, 25].sort()«1»«10»«100»«25»«5»compara como texto, letra a letra: «100» va antes que «25» porque «1» va antes que «2»[10, 1, 5, 100, 25].sort((a, b) => a – b)151025100compara como números: si a – b es negativo, a va antesY ojo: sort() cambia el array original; toSorted() devuelve una copia.

El orden de diccionario y el orden numérico, con los mismos cinco números. Esquema: decodigo.com.

Texto en español: localeCompare

const nombres = ["zoe", "Álvaro", "ana", "Óscar", "Beatriz", "ñu", "nora"];
console.log([...nombres].sort().join(", "));
console.log([...nombres].sort((a, b) => a.localeCompare(b, "es")).join(", "));

const colador = new Intl.Collator("es", { sensitivity: "base" });
console.log([...nombres].sort(colador.compare).join(", "));
console.log(colador.compare("Ana", "ana"), colador.compare("canción", "cancion"));
salidaBeatriz, ana, nora, zoe, Álvaro, Óscar, ñu
Álvaro, ana, Beatriz, nora, ñu, Óscar, zoe
Álvaro, ana, Beatriz, nora, ñu, Óscar, zoe
0 0

El sort() a secas ordena por el código de cada carácter: las mayúsculas van antes que las minúsculas, y las letras con acento y la ñ, al final de todo. localeCompare con "es" sigue las reglas del español. Si vas a ordenar muchos elementos, Intl.Collator hace lo mismo más rápido, porque prepara las reglas una sola vez; con sensitivity: "base", además, considera iguales «Ana» y «ana», o «canción» y «cancion» (por eso los dos ceros).

Objetos por varias claves

const productos = [
  { nombre: "Ratón", categoria: "periféricos", precio: 19.5 },
  { nombre: "Monitor", categoria: "pantallas", precio: 189 },
  { nombre: "Teclado", categoria: "periféricos", precio: 49.9 },
  { nombre: "Webcam", categoria: "periféricos", precio: 49.9 },
  { nombre: "Portátil", categoria: "equipos", precio: 899 },
];

const ordenados = productos.toSorted(
  (a, b) => a.categoria.localeCompare(b.categoria, "es") || b.precio - a.precio
);
for (const p of ordenados) console.log(p.categoria.padEnd(12), String(p.precio).padStart(6), p.nombre);

console.log("el original sigue igual:", productos[0].nombre);
salidaequipos         899 Portátil
pantallas       189 Monitor
periféricos    49.9 Teclado
periféricos    49.9 Webcam
periféricos    19.5 Ratón
el original sigue igual: Ratón

El truco está en el ||: si la categoría es la misma, la primera comparación da 0, que cuenta como falso, y se usa la segunda, el precio de mayor a menor. Teclado y Webcam cuestan lo mismo y conservan su orden original: el ordenamiento de JavaScript es estable, lo garantiza el estándar.

sort cambia el array; toSorted no

const original = [3, 1, 2];
const ordenado = original.sort();
console.log(original, ordenado, original === ordenado);

const otro = [3, 1, 2];
const copia = otro.toSorted();
console.log(otro, copia, otro === copia);
salida[ 1, 2, 3 ] [ 1, 2, 3 ] true
[ 3, 1, 2 ] [ 1, 2, 3 ] false

sort() ordena el array original y además lo devuelve, así que ordenado y original son el mismo array. Es fácil desordenar sin querer datos que se usan en otro sitio. toSorted(), disponible en todos los navegadores actuales y en Node desde la versión 20, devuelve una copia ordenada y deja el original en paz. Lo mismo hacen toReversed() y toSpliced().

Buscar

const productos = [
  { id: 1, nombre: "Ratón", precio: 19.5 },
  { id: 2, nombre: "Monitor", precio: 189 },
  { id: 3, nombre: "Teclado", precio: 49.9 },
  { id: 4, nombre: "Webcam", precio: 49.9 },
];

console.log(productos.find((p) => p.precio > 40));
console.log(productos.findLast((p) => p.precio > 40));
console.log(productos.findIndex((p) => p.nombre === "Teclado"));
console.log(productos.find((p) => p.precio > 1000));
console.log(productos.findIndex((p) => p.precio > 1000));
console.log(productos.filter((p) => p.precio < 50).map((p) => p.nombre));
console.log(productos.some((p) => p.precio > 100), productos.every((p) => p.precio > 10));

const precios = [19.5, 189, 49.9, NaN];
console.log(precios.includes(189), precios.indexOf(189));
console.log(precios.includes(NaN), precios.indexOf(NaN));
salida{ id: 2, nombre: 'Monitor', precio: 189 }
{ id: 4, nombre: 'Webcam', precio: 49.9 }
2
undefined
-1
[ 'Ratón', 'Teclado', 'Webcam' ]
true true
true 1
true -1
MétodoDevuelveSi no encuentra nada
find / findLastEl primer / el último elemento que cumpleundefined
findIndexSu posición-1
filterTodos los que cumplen, en un array nuevo[]
some / everySi alguno / todos cumplenfalse / true
includesSi el valor estáfalse

Las dos últimas líneas de la salida esconden un detalle: includes(NaN) encuentra el NaN, pero indexOf(NaN) devuelve -1. indexOf compara con ===, y NaN === NaN es falso; includes usa otra comparación que sí lo reconoce.

Agrupar

const productos = [
  { nombre: "Ratón", categoria: "periféricos" },
  { nombre: "Monitor", categoria: "pantallas" },
  { nombre: "Teclado", categoria: "periféricos" },
];
const grupos = Object.groupBy(productos, (p) => p.categoria);
console.log(grupos);
salida[Object: null prototype] {
  'periféricos': [
    { nombre: 'Ratón', categoria: 'periféricos' },
    { nombre: 'Teclado', categoria: 'periféricos' }
  ],
  pantallas: [ { nombre: 'Monitor', categoria: 'pantallas' } ]
}

Object.groupBy reparte los elementos según lo que devuelva la función. El «null prototype» indica que el objeto resultante no hereda nada, así que no hay riesgo de que una categoría se llame toString y choque con algo.

Muchas búsquedas: un Map

find recorre el array desde el principio cada vez. Con pocos elementos da igual; si vas a buscar muchas veces en una lista grande, conviene construir un Map una vez:

const productos = Array.from({ length: 100_000 }, (_, i) => ({ id: i + 1, nombre: `producto ${i + 1}` }));

let t = performance.now();
for (let i = 0; i < 1000; i++) productos.find((p) => p.id === 99_999);
console.log("find, 1,000 búsquedas:", (performance.now() - t).toFixed(1), "ms");

const porId = new Map(productos.map((p) => [p.id, p]));
t = performance.now();
for (let i = 0; i < 1000; i++) porId.get(99_999);
console.log("Map.get, 1,000 búsquedas:", (performance.now() - t).toFixed(2), "ms");
console.log(porId.get(99_999).nombre);
salidafind, 1,000 búsquedas: 529.4 ms
Map.get, 1,000 búsquedas: 0.03 ms
producto 99999

Mil búsquedas en cien mil productos: medio segundo con find, frente a unas centésimas de milisegundo con el Map. Lo ejecutamos tres veces y los números apenas cambiaron. Construir el Map tiene un coste, pero se paga una sola vez.

Todo el código de esta página se ejecutó con Node.js 24.22 en un contenedor limpio antes de publicarla; las salidas, incluidos los errores, están copiadas de esa ejecución. Documentación oficial: Array.prototype.sort en MDN.