Iteración sobre arrays
Introducción
La iteración es una de las operaciones más frecuentes que realizamos con arrays: recorrer sus elementos para acceder a ellos, transformarlos o realizar alguna operación basada en sus valores. JavaScript ofrece múltiples formas de iterar sobre arrays, desde los bucles tradicionales hasta métodos funcionales más modernos y expresivos.
Cada enfoque tiene sus propias ventajas y casos de uso óptimos. Mientras que los bucles tradicionales ofrecen máximo control y flexibilidad, los métodos funcionales como forEach
, map
, filter
y reduce
nos permiten escribir código más declarativo y conciso, expresando claramente nuestra intención.
En este artículo exploraremos las diferentes técnicas para recorrer arrays en JavaScript, analizando cuándo usar cada una, comparando su rendimiento y viendo cómo podemos combinarlas para procesar datos de manera eficiente y elegante.
Bucle for clásico con arrays
El bucle for
tradicional es la forma más básica y versátil de iterar sobre un array. Nos da control total sobre el proceso de iteración, permitiéndonos controlar el índice y la dirección.
Sintaxis básica
const frutas = ["manzana", "pera", "naranja", "plátano"];
for (let i = 0; i < frutas.length; i++) {
console.log(`Fruta ${i}: ${frutas[i]}`);
}
// Fruta 0: manzana
// Fruta 1: pera
// Fruta 2: naranja
// Fruta 3: plátano
Variantes del bucle for
Recorrer en sentido inverso
const numeros = [10, 20, 30, 40, 50];
for (let i = numeros.length - 1; i >= 0; i--) {
console.log(`Posición ${i}: ${numeros[i]}`);
}
// Posición 4: 50
// Posición 3: 40
// Posición 2: 30
// Posición 1: 20
// Posición 0: 10
Saltar elementos
const numeros = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// Iterar solo por los elementos en posiciones pares
for (let i = 0; i < numeros.length; i += 2) {
console.log(numeros[i]);
}
// 0
// 2
// 4
// 6
// 8
Optimización del bucle for
Un patrón común para optimizar ligeramente el rendimiento es guardar la longitud del array en una variable:
const elementos = ["a", "b", "c", "d", "e"];
// En cada iteración se evalúa elementos.length
for (let i = 0; i < elementos.length; i++) {
console.log(elementos[i]);
}
// Más eficiente: se evalúa elementos.length solo una vez
const longitud = elementos.length;
for (let i = 0; i < longitud; i++) {
console.log(elementos[i]);
}
Esta optimización es más relevante en arrays muy grandes o en bucles que se ejecutan con frecuencia.
Pros y contras del bucle for clásico
Ventajas:
- Máximo control sobre el proceso de iteración
- Acceso al índice de cada elemento
- Posibilidad de modificar el índice durante la iteración
- Posibilidad de recorrer en cualquier dirección o saltar elementos
- Rendimiento ligeramente superior en navegadores antiguos
Desventajas:
- Sintaxis más verbosa
- Mayor posibilidad de errores (índices fuera de rango, condiciones incorrectas)
- Código menos declarativo y expresivo
- Anidación de bucles puede reducir la legibilidad
Método forEach
El método forEach()
proporciona una forma más moderna y declarativa de iterar sobre arrays. Ejecuta una función (callback) para cada elemento del array.
Sintaxis básica
const colores = ["rojo", "verde", "azul"];
colores.forEach(function(color) {
console.log(color);
});
// rojo
// verde
// azul
Con funciones flecha (ES6) se vuelve aún más conciso:
colores.forEach(color => console.log(color));
Parámetros adicionales del callback
El callback en forEach
puede recibir hasta tres parámetros:
- El elemento actual
- El índice del elemento
- El array completo que estamos recorriendo
const numeros = [10, 20, 30];
numeros.forEach((valor, indice, array) => {
console.log(`array[${indice}] = ${valor}`);
console.log(`array completo: [${array}]`);
});
// array[0] = 10
// array completo: [10,20,30]
// array[1] = 20
// array completo: [10,20,30]
// array[2] = 30
// array completo: [10,20,30]
Casos de uso comunes
Actualizar elementos de un array
const precios = [100, 200, 300, 400];
// Aplicar un descuento del 10%
precios.forEach((precio, indice, array) => {
array[indice] = precio * 0.9;
});
console.log(precios); // [90, 180, 270, 360]
Copiar elementos a otra estructura
const nombres = ["Ana", "Carlos", "Elena"];
const longitudesNombres = {};
nombres.forEach(nombre => {
longitudesNombres[nombre] = nombre.length;
});
console.log(longitudesNombres); // {Ana: 3, Carlos: 6, Elena: 5}
Limitaciones de forEach
- No puedes detener la iteración (no hay equivalente a
break
) - No puedes saltar a la siguiente iteración (no hay equivalente a
continue
) - No devuelve un valor (el valor de retorno del callback se ignora)
- Siempre recorre todos los elementos del array
// Ejemplo: intentando usar "return" para detener la iteración
const numeros = [1, 2, 3, 4, 5];
numeros.forEach(numero => {
console.log(numero);
if (numero === 3) {
return; // Esto solo sale de la función actual, no detiene la iteración
}
});
// 1
// 2
// 3
// 4
// 5
Métodos funcionales (map, filter, reduce)
Estos métodos son más especializados que forEach
y generalmente más útiles, ya que además de iterar, realizan transformaciones específicas y devuelven nuevos resultados.
map()
El método map()
crea un nuevo array con los resultados de aplicar una función a cada elemento del array original.
const numeros = [1, 2, 3, 4, 5];
// Crear un nuevo array con cada número duplicado
const duplicados = numeros.map(numero => numero * 2);
console.log(duplicados); // [2, 4, 6, 8, 10]
console.log(numeros); // [1, 2, 3, 4, 5] - el original no cambia
Casos de uso de map()
:
Transformar objetos
const personas = [
{ nombre: "Ana", edad: 28 },
{ nombre: "Carlos", edad: 32 },
{ nombre: "Elena", edad: 24 }
];
const nombresPersonas = personas.map(persona => persona.nombre);
console.log(nombresPersonas); // ["Ana", "Carlos", "Elena"]
// Crear objetos con estructura diferente
const resumenes = personas.map(persona => ({
nombreCompleto: persona.nombre,
esAdulto: persona.edad >= 18
}));
console.log(resumenes);
// [
// { nombreCompleto: "Ana", esAdulto: true },
// { nombreCompleto: "Carlos", esAdulto: true },
// { nombreCompleto: "Elena", esAdulto: true }
// ]
Trabajar con strings
const palabras = ["javascript", "html", "css"];
const mayusculas = palabras.map(palabra => palabra.toUpperCase());
console.log(mayusculas); // ["JAVASCRIPT", "HTML", "CSS"]
filter()
El método filter()
crea un nuevo array con los elementos que cumplen una condición especificada.
const numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Filtrar los números pares
const pares = numeros.filter(numero => numero % 2 === 0);
console.log(pares); // [2, 4, 6, 8, 10]
console.log(numeros); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - el original no cambia
Casos de uso de filter()
:
Filtrar objetos por una propiedad
const productos = [
{ nombre: "Laptop", precio: 900, disponible: true },
{ nombre: "Teléfono", precio: 400, disponible: false },
{ nombre: "Monitor", precio: 300, disponible: true },
{ nombre: "Teclado", precio: 50, disponible: true },
{ nombre: "Tableta", precio: 250, disponible: false }
];
// Filtrar productos disponibles
const disponibles = productos.filter(producto => producto.disponible);
console.log(disponibles.length); // 3
// Filtrar productos económicos y disponibles
const economicosDisponibles = productos.filter(producto =>
producto.precio < 300 && producto.disponible
);
console.log(economicosDisponibles);
// [{ nombre: "Teclado", precio: 50, disponible: true }]
Eliminar elementos no deseados
const valores = [0, 1, false, 2, "", 3, null, undefined, 4, NaN];
// Filtrar valores "truthy" (eliminar valores "falsy")
const valoresTruthy = valores.filter(valor => !!valor);
console.log(valoresTruthy); // [1, 2, 3, 4]
// Eliminar valores nulos o indefinidos
const valoresValidos = valores.filter(valor => valor !== null && valor !== undefined);
console.log(valoresValidos); // [0, 1, false, 2, "", 3, 4, NaN]
reduce()
El método reduce()
aplica una función acumuladora a cada elemento del array (de izquierda a derecha), reduciéndolo a un único valor.
const numeros = [1, 2, 3, 4, 5];
// Sumar todos los números
const suma = numeros.reduce((acumulador, valorActual) => acumulador + valorActual, 0);
console.log(suma); // 15
El segundo parámetro de reduce()
(0 en el ejemplo) es el valor inicial del acumulador. Si se omite, se usa el primer elemento del array como valor inicial y se comienza desde el segundo elemento.
Casos de uso de reduce()
:
Calcular estadísticas
const ventas = [120, 80, 200, 100, 150];
// Calcular total
const totalVentas = ventas.reduce((total, venta) => total + venta, 0);
console.log(totalVentas); // 650
// Calcular promedio
const promedio = totalVentas / ventas.length;
console.log(promedio); // 130
// Encontrar el valor máximo
const ventaMaxima = ventas.reduce((max, venta) => venta > max ? venta : max, ventas[0]);
console.log(ventaMaxima); // 200
Agrupar y contar
const frutasCompradas = ["manzana", "plátano", "manzana", "naranja", "manzana", "plátano"];
// Contar ocurrencias de cada fruta
const conteoFrutas = frutasCompradas.reduce((conteo, fruta) => {
conteo[fruta] = (conteo[fruta] || 0) + 1;
return conteo;
}, {});
console.log(conteoFrutas);
// { manzana: 3, plátano: 2, naranja: 1 }
Aplanar arrays anidados
const arrayAnidado = [[1, 2], [3, 4], [5, 6]];
const aplanado = arrayAnidado.reduce((acumulador, actual) => {
return acumulador.concat(actual);
}, []);
console.log(aplanado); // [1, 2, 3, 4, 5, 6]
Bucles for...of
Introducido en ES6, el bucle for...of
proporciona una sintaxis limpia y simple para iterar sobre elementos iterables, incluyendo arrays.
Sintaxis básica
const colores = ["rojo", "verde", "azul"];
for (const color of colores) {
console.log(color);
}
// rojo
// verde
// azul
Obtener índices con entries()
A diferencia del bucle for
clásico, for...of
no proporciona acceso directo al índice. Sin embargo, podemos usar el método entries()
para obtener pares [índice, valor]:
const frutas = ["manzana", "pera", "naranja"];
for (const [indice, fruta] of frutas.entries()) {
console.log(`${indice}: ${fruta}`);
}
// 0: manzana
// 1: pera
// 2: naranja
Ventajas de for...of
- Sintaxis limpia y concisa
- Funciona con todos los objetos iterables (arrays, strings, maps, sets, etc.)
- Puedes usar
break
ycontinue
- Maneja correctamente arrays dispersos (sparse arrays)
- Menos propenso a errores que el bucle
for
clásico
// Ejemplo con break
const numeros = [1, 2, 3, 4, 5];
for (const numero of numeros) {
if (numero > 3) {
break; // Salir del bucle
}
console.log(numero);
}
// 1
// 2
// 3
// Ejemplo con continue
for (const numero of numeros) {
if (numero % 2 === 0) {
continue; // Saltar a la siguiente iteración
}
console.log(numero);
}
// 1
// 3
// 5
Comparativa de rendimiento entre métodos
En términos generales, el rendimiento de los diferentes métodos de iteración sigue aproximadamente este orden (de más rápido a más lento):
- Bucle
for
clásico - Bucle
for...of
- Método
forEach
- Métodos funcionales (
map
,filter
,reduce
)
Sin embargo, las diferencias de rendimiento suelen ser mínimas en la mayoría de casos prácticos, y solo se vuelven relevantes con arrays muy grandes o en operaciones muy frecuentes.
// Ejemplo básico de medición de rendimiento
const arrayGrande = Array(1000000).fill(1);
console.time('For clásico');
let suma1 = 0;
for (let i = 0; i < arrayGrande.length; i++) {
suma1 += arrayGrande[i];
}
console.timeEnd('For clásico');
console.time('For...of');
let suma2 = 0;
for (const num of arrayGrande) {
suma2 += num;
}
console.timeEnd('For...of');
console.time('forEach');
let suma3 = 0;
arrayGrande.forEach(num => {
suma3 += num;
});
console.timeEnd('forEach');
console.time('reduce');
const suma4 = arrayGrande.reduce((acc, val) => acc + val, 0);
console.timeEnd('reduce');
Los resultados exactos variarán según el navegador y el dispositivo, pero generalmente siguen el patrón mencionado.
Interrumpir o saltar iteraciones
Las diferentes técnicas de iteración tienen distintas capacidades para controlar el flujo:
Método | break | continue | return |
---|---|---|---|
for | Soportado | Soportado | Termina la función contenedora |
for...of | Soportado | Soportado | Termina la función contenedora |
forEach | No soportado | No soportado | Solo sale de la iteración actual |
map/filter/reduce | No soportado | No soportado | Solo afecta al valor devuelto por el callback |
// Alternativa para "break" con forEach: usar try/catch
try {
[1, 2, 3, 4, 5].forEach(num => {
console.log(num);
if (num > 3) {
throw new Error("BreakError");
}
});
} catch (e) {
if (e.message !== "BreakError") throw e;
}
// 1
// 2
// 3
// 4
Esta técnica con try/catch
no es recomendable para uso general, ya que las excepciones están diseñadas para manejar errores, no para control de flujo. En estos casos, es mejor usar un bucle for
o for...of
.
Encadenamiento de métodos
Una de las grandes ventajas de los métodos funcionales es que pueden encadenarse para realizar transformaciones complejas de forma concisa:
const productos = [
{ id: 1, nombre: "Laptop", precio: 900, categoria: "Electrónica" },
{ id: 2, nombre: "Camisa", precio: 30, categoria: "Ropa" },
{ id: 3, nombre: "Monitor", precio: 300, categoria: "Electrónica" },
{ id: 4, nombre: "Pantalón", precio: 45, categoria: "Ropa" },
{ id: 5, nombre: "Teclado", precio: 50, categoria: "Electrónica" }
];
// Filtrar productos de electrónica, obtener sus nombres y ordenarlos alfabéticamente
const nombresElectronica = productos
.filter(producto => producto.categoria === "Electrónica")
.map(producto => producto.nombre)
.sort();
console.log(nombresElectronica); // ["Laptop", "Monitor", "Teclado"]
Optimización del encadenamiento
Cuando encadenamos métodos, debemos considerar el orden para optimizar el rendimiento:
const numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Menos eficiente: map se ejecuta 10 veces, filter 10 veces
const resultado1 = numeros
.map(n => n * 2)
.filter(n => n > 10);
// Más eficiente: filter se ejecuta 10 veces, map solo 5 veces
const resultado2 = numeros
.filter(n => n > 5)
.map(n => n * 2);
Regla general: Cuando combinamos filter
y map
, es más eficiente filtrar primero y luego mapear, especialmente si el filtro elimina muchos elementos.
Patrones comunes de procesamiento
Transformación de datos
// Procesar datos de usuarios para la vista
const usuarios = [
{ id: 1, nombre: "Ana", apellido: "García", edad: 28 },
{ id: 2, nombre: "Carlos", apellido: "Pérez", edad: 32 },
{ id: 3, nombre: "Elena", apellido: "Martínez", edad: 24 }
];
const datosVista = usuarios.map(usuario => ({
id: usuario.id,
nombreCompleto: `${usuario.nombre} ${usuario.apellido}`,
edadEnMeses: usuario.edad * 12
}));
console.log(datosVista);
// [
// {id: 1, nombreCompleto: "Ana García", edadEnMeses: 336},
// {id: 2, nombreCompleto: "Carlos Pérez", edadEnMeses: 384},
// {id: 3, nombreCompleto: "Elena Martínez", edadEnMeses: 288}
// ]
Filtrado y validación
// Filtrar transacciones sospechosas
const transacciones = [
{ id: "TX001", monto: 1200, aprobada: true },
{ id: "TX002", monto: 25000, aprobada: false },
{ id: "TX003", monto: 300, aprobada: true },
{ id: "TX004", monto: 18000, aprobada: true },
{ id: "TX005", monto: 500, aprobada: true }
];
const transaccionesSospechosas = transacciones.filter(tx =>
tx.monto > 10000 && tx.aprobada
);
console.log(transaccionesSospechosas);
// [{ id: "TX004", monto: 18000, aprobada: true }]
Agrupación y cálculos estadísticos
const ventas = [
{ producto: "Laptop", region: "Norte", monto: 1200 },
{ producto: "Monitor", region: "Sur", monto: 800 },
{ producto: "Laptop", region: "Este", monto: 1100 },
{ producto: "Teclado", region: "Norte", monto: 300 },
{ producto: "Monitor", region: "Este", monto: 700 },
{ producto: "Teclado", region: "Sur", monto: 250 }
];
// Agrupar ventas por producto
const ventasPorProducto = ventas.reduce((grupos, venta) => {
const { producto, monto } = venta;
if (!grupos[producto]) {
grupos[producto] = {
total: 0,
cantidad: 0
};
}
grupos[producto].total += monto;
grupos[producto].cantidad++;
grupos[producto].promedio = grupos[producto].total / grupos[producto].cantidad;
return grupos;
}, {});
console.log(ventasPorProducto);
// {
// Laptop: {total: 2300, cantidad: 2, promedio: 1150},
// Monitor: {total: 1500, cantidad: 2, promedio: 750},
// Teclado: {total: 550, cantidad: 2, promedio: 275}
// }
Resumen
JavaScript ofrece múltiples formas de iterar sobre arrays, cada una con sus propias ventajas y casos de uso ideales:
-
Bucle for clásico: Ofrece máximo control y mejor rendimiento, ideal para operaciones complejas donde necesitamos manipular el índice o interrumpir el bucle.
-
Método forEach: Proporciona una sintaxis más limpia y declarativa, ideal para operaciones simples donde queremos ejecutar una acción para cada elemento.
-
Métodos funcionales (map, filter, reduce): Son especializados para transformaciones específicas, creando nuevos arrays o valores sin modificar el original. Son ideales para programación funcional y código declarativo.
-
Bucle for...of: Ofrece una sintaxis moderna y sencilla, compatible con
break
ycontinue
, ideal para iterar sobre cualquier objeto iterable.
La elección del método dependerá del contexto, priorizando la legibilidad y expresividad del código en la mayoría de los casos, y considerando el rendimiento solo cuando sea un factor crítico.
El encadenamiento de métodos funcionales nos permite expresar transformaciones complejas de forma concisa y legible, siendo una de las técnicas más poderosas para procesar datos en arrays.
En el próximo artículo, profundizaremos en los arrays multidimensionales, explorando cómo trabajar con estructuras de datos más complejas como matrices y árboles representados mediante arrays anidados.