map(), filter() y reduce() no son intercambiables: el resultado esperado decide el método

Los métodos de arrays de JavaScript suelen enseñarse como una colección de verbos: map(), filter(), reduce(), find() e includes(). El problema aparece cuando todos empiezan a parecer intercambiables. Un forEach() que intenta devolver una lista, un reduce() que reemplaza cualquier método y un filter() usado para preguntar si existe un elemento pueden producir código que funciona en un caso y confunde al siguiente lector.

La pregunta útil no es “¿qué método está de moda?”, sino “¿qué resultado quiero comunicar?”. La referencia de Array en MDN recuerda que los arrays son objetos indexados, redimensionables y con operaciones de recorrido, copia y mutación. A partir de esa base, elegir un método es decidir la forma del resultado y si la operación debe detenerse pronto o recorrer toda la colección.

Primero describe el resultado en una frase

Supón esta colección:

const pedidos = [
  { id: 1, total: 35, estado: "pagado" },
  { id: 2, total: 12, estado: "pendiente" },
  { id: 3, total: 80, estado: "pagado" }
];

Antes de escribir el método, termina una de estas frases:

FraseResultado esperadoMétodo que expresa la intención
“Quiero ejecutar algo por cada pedido”Normalmente nada nuevoforEach() o un bucle
“Quiero un pedido transformado por cada pedido”Array de la misma longitudmap()
“Quiero conservar solo algunos pedidos”Array igual o más cortofilter()
“Quiero el primer pedido que cumple”Elemento o undefinedfind()
“Quiero saber si existe este valor”Booleanoincludes()
“Quiero combinar todos en un solo resultado”Valor acumuladoreduce()

La tabla es un punto de partida, no una prohibición. Un bucle puede ser más legible cuando hay varias decisiones y reduce() puede ser correcto para construir un índice. Lo importante es que el método no oculte la forma del resultado que el resto de la función necesita.

map() transforma; no es un filtro disfrazado

map() crea un nuevo array con el resultado de llamar una función para cada elemento. Si la lista tiene tres posiciones, el resultado normalmente tiene tres posiciones:

const importes = pedidos.map((pedido) => pedido.total);
// [35, 12, 80]

const tarjetas = pedidos.map((pedido) => ({
  texto: `Pedido #${pedido.id}`,
  destacado: pedido.total >= 50
}));

La referencia de map() en MDN describe precisamente la creación de un nuevo array a partir de los resultados de la función. Si dentro del callback devuelves undefined en algunos casos, no has eliminado esos elementos: has creado huecos lógicos o valores indefinidos que otra parte deberá interpretar.

Cuando necesitas transformar y excluir, puedes encadenar filter().map(), pero piensa si la lectura sigue siendo clara:

const pagados = pedidos
  .filter((pedido) => pedido.estado === "pagado")
  .map((pedido) => pedido.total);

La primera operación decide pertenencia. La segunda decide representación. Esa separación es más auditable que un callback que intenta hacer las dos cosas a la vez.

filter() expresa pertenencia y conserva el tipo de colección

filter() recibe una función que debe responder si cada elemento se conserva. Devuelve un nuevo array, incluso si el resultado queda vacío:

const pendientes = pedidos.filter(
  (pedido) => pedido.estado === "pendiente"
);

Según la referencia de filter() de MDN, el método crea una copia superficial de la parte que pasa la prueba. Eso tiene dos consecuencias prácticas. No modifica el array exterior original, pero los objetos que contiene pueden seguir siendo las mismas referencias. Si después alteras un pedido dentro del resultado, puedes estar alterando también el pedido de la lista original.

Usa filter() cuando el resultado debe seguir siendo una colección. Si solo quieres saber si hay al menos un pedido pendiente, some() comunica mejor esa intención y puede detenerse cuando encuentra una coincidencia. Crear un array completo para convertirlo después en booleano añade una etapa que nadie necesita.

find() e includes() responden preguntas de existencia diferentes

find() busca un elemento mediante una condición y devuelve el primero que la cumple. includes() pregunta si el array contiene un valor determinado y devuelve true o false:

const pedidoGrande = pedidos.find((pedido) => pedido.total > 50);
const estados = ["pagado", "pendiente"];
const tieneCancelado = estados.includes("cancelado");

La diferencia no es una cuestión de estilo. find() te entrega el registro que necesitas usar; includes() te entrega una respuesta sobre una coincidencia de valor. Para una colección de objetos, includes() solo encuentra la misma referencia, no cualquier objeto con el mismo contenido:

const a = { id: 1 };
const b = { id: 1 };

[a].includes(b); // false
[a].includes(a); // true

Si la pregunta es “¿existe un pedido con este id?”, usa some() o find() con una condición sobre id. Si confundes igualdad de referencia con igualdad de datos, el método parecerá fallar cuando en realidad estaba respondiendo otra pregunta.

reduce() no es el método “más profesional”

reduce() combina los elementos de un array en un acumulador. Puede sumar, agrupar, indexar o construir otra estructura, pero su flexibilidad también permite esconder demasiadas decisiones en una sola expresión:

const total = pedidos.reduce(
  (acumulado, pedido) => acumulado + pedido.total,
  0
);

En la referencia de reduce() de MDN, el valor inicial forma parte de la definición del acumulador. Proporcionarlo explícitamente evita que el primer elemento se convierta accidentalmente en una semilla con un tipo distinto y hace que el caso de array vacío sea predecible.

El mismo método puede crear un índice por id:

const porId = pedidos.reduce((indice, pedido) => {
  indice[pedido.id] = pedido;
  return indice;
}, {});

Este código funciona, pero también introduce mutación dentro del acumulador. Otra versión puede construir un objeto nuevo en cada paso, aunque esa decisión tiene otro coste. No hay una medalla por usar menos líneas: el criterio es si quien lee entiende qué estructura sale y qué coste acepta.

Si un reduce() contiene condiciones anidadas, varios tipos posibles y nombres vagos como acc o item, prueba a dividirlo en una función o usar un bucle explícito. El código declarativo solo ayuda cuando la declaración es más clara que el procedimiento.

forEach() tiene un lugar, pero no devuelve una colección

forEach() sirve para efectos: registrar, actualizar una interfaz, enviar una métrica o llamar una función que ya tiene una responsabilidad externa. Su retorno no es la lista transformada:

const resultados = pedidos.forEach((pedido) => {
  console.log(pedido.id);
});

console.log(resultados); // undefined

Si necesitas esperar operaciones asíncronas, tampoco conviertas automáticamente forEach() en un bucle con await. Para ordenar tareas o ejecutarlas en paralelo necesitas otra estructura, como un bucle for...of o una colección de promises coordinada con Promise.all(). La forma del método debe coincidir con la forma del trabajo.

La decisión también incluye mutabilidad y coste

Los métodos map(), filter() y muchas operaciones modernas producen nuevos arrays, pero no convierten objetos interiores en copias profundas. sort(), reverse() y splice(), en cambio, modifican el array en el que se ejecutan. Antes de encadenar métodos, responde tres preguntas:

  1. ¿El array original debe conservarse para otra parte del programa?
  2. ¿El resultado contiene objetos que podrían seguir compartiendo referencias?
  3. ¿La colección puede ser suficientemente grande para que varias pasadas sean relevantes?

Una sola pasada no siempre es mejor si obliga a descifrar un acumulador opaco. Varias operaciones pequeñas pueden ser más fáciles de probar y mantener. Cuando el rendimiento importe, mide con datos cercanos al caso real en vez de convertir una intuición sobre métodos “rápidos” en una regla general.

Un ejercicio: procesa pedidos sin perder la intención

Construye una función que reciba pedidos y devuelva una factura pequeña con cuatro campos: cantidad de pedidos pagados, total pagado, primer pedido superior a 50 y si existe algún pedido pendiente. Una solución comprensible puede usar varios métodos porque cada respuesta tiene una forma distinta:

function resumirPedidos(pedidos) {
  const pagados = pedidos.filter(
    (pedido) => pedido.estado === "pagado"
  );

  return {
    cantidadPagados: pagados.length,
    totalPagado: pagados.reduce(
      (total, pedido) => total + pedido.total,
      0
    ),
    primerPedidoGrande: pedidos.find(
      (pedido) => pedido.total > 50
    ),
    hayPendientes: pedidos.some(
      (pedido) => pedido.estado === "pendiente"
    )
  };
}

No es la única implementación posible. Su ventaja es que cada propiedad responde a una pregunta concreta. Si una prueba falla, sabrás si el problema está en pertenencia, acumulación, búsqueda o presencia. La explicación no depende de recordar cuál método “se usa normalmente”, sino de conectar el método con el tipo de resultado.

Una cadena legible también necesita una frontera

Encadenar métodos puede hacer que una transformación se lea como una frase, pero cada etapa crea una colección intermedia y conserva referencias a los objetos interiores. Para listas pequeñas, la claridad suele compensar ese coste. Para una colección grande o una ruta que se ejecuta muchas veces, observa cuántos elementos recorres y cuándo creas resultados que solo se usan durante una etapa.

const nombres = pedidos
  .filter((pedido) => pedido.estado === "pagado")
  .map((pedido) => pedido.cliente)
  .filter(Boolean);

Esta cadena es fácil de leer porque cada operación tiene una responsabilidad. Pero también puedes necesitar una sola pasada si el código está en un camino crítico y la medición demuestra que las colecciones intermedias importan. La alternativa no debe sacrificar la intención:

const nombres = [];

for (const pedido of pedidos) {
  if (pedido.estado === "pagado" && pedido.cliente) {
    nombres.push(pedido.cliente);
  }
}

Ninguna versión es automáticamente superior. La primera comunica “selecciona y proyecta”; la segunda hace visible un recorrido con un efecto local. Si eliges el bucle, explica por qué el orden de las condiciones y la mutación del array resultado son parte del diseño. Si eliges la cadena, evita esconder una función enorme dentro de un callback.

También revisa la mutabilidad de la respuesta. filter() y map() crean arrays nuevos, pero si después llamas a un método mutador sobre un objeto interior, todavía puedes alterar la fuente. Cuando una función promete no modificar sus entradas, una prueba debe comprobarlo de forma explícita.

Continúa desde datos y no desde la lista de métodos

Si todavía estás consolidando el modelo de objetos que aparece dentro de los arrays, revisa nuestra guía sobre objetos en JavaScript. Para llevar estos resultados a una página, la guía de manipulación del DOM ayuda a separar el procesamiento de datos del renderizado. Y si tus arrays llegan desde formularios, consulta las etiquetas HTML esenciales antes de asumir que toda entrada tiene la misma forma.

Preguntas para elegir sin memorizar una receta

¿Cuándo debo usar map()?

Cuando necesitas un resultado por cada elemento y esperas una colección de la misma longitud, aunque sus valores puedan cambiar.

¿filter() modifica el array original?

No modifica el array exterior y devuelve una copia superficial de los elementos que pasan la prueba. Los objetos interiores pueden seguir compartiendo referencias.

¿Por qué reduce() necesita un valor inicial?

Un valor inicial hace explícito el tipo y el estado vacío del acumulador. Sin él, el primer elemento se usa como acumulador y el array vacío puede producir un error.

¿find() y includes() son equivalentes?

No. find() devuelve el primer elemento que satisface una condición; includes() devuelve un booleano al comprobar la presencia de un valor.

La mejor señal de que elegiste bien no es que el código tenga menos líneas. Es que un lector pueda predecir la forma del resultado antes de abrir la documentación del método.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio