Escuela Frontend
JavaScript

La Guía Definitiva de Métodos de Arreglos

Matías Hernández Arellano
Autor
Matías Hernández Arellano

Los arreglos, esa sencilla estructura de datos indexada, que en Javascript se crea simplemente con el uso de [] ,son esenciales al querer renderizar y manipular elementos en la interfaz. Casi todos los datos que necesitamos mostrar en pantalla pueden ser representados como una colección, es decir, un arreglo.

Javascript, siendo un lenguaje demasiado flexible, ofrece múltiples métodos para manipular arreglos, algunos de ellos mutables (como los mencionados en la entrega anterior) y otros inmutables.

Nos enfocaremos en los métodos inmutables que en mi experiencia son los que más utilizarás en tu día a día desarrollando aplicaciones.

Algo que todos estos métodos tienen en común es que son parte del prototipo de arreglo, es decir, son accesibles como métodos directamente desde un arreglo. Además, todos reciben una función tipo “callback” que el método utilizará de forma interna aplicando dicha función en cada iteración y en cada ítem del arreglo. Esto es: estos métodos son declarativos.

En todos los ejemplos trabajaremos con esta estructura inicial.

const users = [{
id: 'efe5f844-788f-42e3-8706-ae7312958576',
username: 'Justin Elliott',
twitter: '@justinElliot',
email: 'justin.elliott@example.com'
},{
id: '309b8b06-b5f5-42a2-9808-8bda5da90fb9',
username: 'Paul C Wiggins',
twitter: '@RealPaul',
email: 'paul.wiggins@example.com'
},
{
id: '47b793e4-d1cd-4ff7-8f85-37c5c3268fc0',
username: 'Margaret J Pitre',
twitter: '@mpitre',
email: 'margaret.pitre@example.com'
},
{
id: 'a34b752c-2ac7-420e-ab0b-8ccf8d18deb6',
username: 'Sharon J Jenkins',
twitter: null,
email: 'sharon.jenkins@example.com'
}
]

Array.find

Este método te permite encontrar un único (y primer) elemento dentro de un arreglo. Para definir qué elementos buscas debes determinar una condición que retorne true o false. :)

Si la condición retorna true entonces significa que el elemento fue encontrado, find deja de iterar sobre el arreglo y retornará el elemento. Si la condición retorna false entonces, el elemento actual no es el buscado, find continuará iterando hasta el final. Si no encuentra el elemento, retornará undefined.

const id = '309b8b06-b5f5-42a2-9808-8bda5da90fb9';
const id2 = 'b5f5-42a2-9808-8bda5da90fb9-309b8b06'
const searchItem = (id) => item => item.id === id
const myUser = users.find(searchItem(id))
console.log('Found: ', myUser)
const myUser2 = users.find(searchItem(id2))
console.log('Not found', myUser2)

En este ejemplo, buscamos dos usuarios identificados por el atributo id.

Para eso creamos una función llamada searchItem que recibe un id y retorna una función. La función retornada acepta un item y luego hace la comparación entre item.id y el parámetro id.

El método find retornará el elemento cuyo id sea igual al id que hemos definido. En el primer caso encuentra un usuario, en el segundo caso, no encuentra resultados y retorna undefined.

Por cierto, la función searchItem puede ser escrita de muchas formas.

const searchItem = (id) => (item) => item.id === id;
// es lo mismo
const searchItem = (id) => (item) => {
return item.id === id;
}
//
const searchItem = (id) => function getItem(item){
return item.id === id;
}
//
function searchItem(id){
return function getItem(item) {
return item.id === id;
}
}
// Recuerdas que hablamos de closures en uno de los primeros emails?
// Y finalmente
users.find(searchItem(id)
// es lo mismo que
users.find(item => item.id === item)

La primera función utiliza el concepto de "currying”. Esta es una técnica nacida de la programación funcional para convertir una función que tiene múltiples argumentos en una serie de funciones de un solo argumento, es decir, por cada argumento se retorna una nueva función que recibe un nuevo argumento.

En la primera función del ejemplo es la forma “corta” de escribir lo mismo que está en la línea 13.

Array.some

Este método también recibe una función callback o “función de prueba”. Retornará true si al menos uno de los elementos del arreglo pasa la prueba.

const isThereAnyJustin = users.some(item => item.username.includes('Justin'))
console.log({ isThereAnyJustin }) // true

En este ejemplo, creamos una variable booleana isThereAnyJustin y luego en la función de prueba revisamos si la propiedad username del ítem actual incluye la palabra Justin. Como al menos uno de los elementos cumple con la función de prueba, se retorna true.

Array.every

Al contrario que el método anterior, Array.every retorna true sólo cuando todos los elementos del arreglo "cumplen" con la función de prueba, y retorna false en caso contrario.

const doAllHaveTwitter = users.every(item => item.twitter)
console.log({ doAllHaveTwitter })
const doAllHaveUsername = users.every(item => item.username)
console.log({ doAllHaveUsername })

Aquí podemos ver dos usos de Array.every a modo de verificador. En el primer caso creamos una variable para guardar el resultado de "testear" si todos los elementos tienen o no la propiedad/atributo twitter, como no es cierto para todos (el último elemento tiene la propiedad como null) tenemos como resultado false. Al contrario, para el segundo caso el resultado es true porque todos los elementos tienen la propiedad username.

Array.includes

Este método te permite determinar si un arreglo contiene o no determinado elemento, retornando true or false

Es casi idéntico al método String.prototype.includes y en efecto hacen lo mismo determinar si un elemento, en este caso un carácter o sub-string está dentro del String.

Lamentablemente includes no sirve para revisar si un determinado arreglo contiene o no un objeto, ya que includes hace una comparación por referencia y un objeto puede ser idéntico a otro en términos de propiedades, pero tener referencias distintas. En este caso, es más útil utilizar Array.some.

const letters = ['a','B','c','D','E']
const haveI = letters.includes('i')
const haveE = letters.includes('E')
console.log({ haveI, haveE })

En este ejemplo, tenemos un arreglo que contiene letras y dos constantes, haveI y haveE que revisan si el arreglo contiene o no una letra en particular.

Array.map

Iniciamos ahora a revisar algunos de los métodos más poderosos para manipular arreglos, en general los anteriores métodos se componen o se basan en estos 3. Array.map crea un nuevo arreglo basado en los resultados de la función que se le pasa como callback, es decir, nos sirve para transformar un arreglo de una forma a otra.

La función callback se ejecuta en cada ítem y el nuevo arreglo se va formando con lo que dicha función retorna.

Esta función callback puede recibir hasta 3 argumentos, siendo el más importante el primero que hace referencia al elemento actual en la iteración, además de eso:

  • index: El índice del elemento actual
  • array: El arreglo sobre el que fue llamado.
const newUsers = users.map(item => {
return {
twitter: item.twitter
}
})
console.log(newUsers)

En este sencillo ejemplo, creamos un nuevo arreglo basado en el arreglo original, pero solo lo contiene la propiedad Twitter.

También podemos crear un nuevo arreglo que aumente el arreglo original.

const newUsers2 = users.map(item => {
return {
...item,
date: (new Date()).toLocaleDateString()
}
})
console.log(newUsers2)

Aquí utilizamos el operador spread ... para expandir el elemento original item y agregar una nueva propiedad date que contendrá la fecha en formato localizado

Dentro de la función callback puedes hacer prácticamente lo que quieras para generar un nuevo arreglo, utilizar otras funciones, condiciones etc. Pero ten cuidado con lo que retornas.

const newUsers3 = users.map(item => {
if(item.twitter) {
return {
...item,
date: (new Date()).toLocaleDateString()
}
}
})
console.log(newUsers3)

En este ejemplo, nuestra función callback contiene un bloque condicional if que revisa si la propiedad twitter del elemento está o no presente. Recordemos que en nuestro arreglo original tenemos un elemento cuya propiedad twitter es null. Array.map crea el nuevo arreglo en base al valor de retorno ¿Qué pasa si no se retorna nada?

Al no retornar nada, como en este caso, el nuevo arreglo contendrá un elemento undefined que quizá no es lo que buscabas y lo que nos lleva al siguiente método.

Array.filter

Este método también crea un nuevo arreglo (todos estos métodos son inmutables y declarativos), pero a diferencia de map, el nuevo arreglo creado no tendrá necesariamente el mismo número de elementos del arreglo original. Sólo aquellos elementos que pasen la “prueba” serán retornados al nuevo arreglo. Un filtro.

Tomando el ejemplo anterior, newUsers3 contiene un valor undefined y podemos eliminarlo utilizando Array.filter así:

const filtrado = newUsers3.filter(item => item)
console.log({ filtrado })

Lo que hace nuestra función se ve un poco extraño, pero esto se basa en la idea que revisamos hace algunas entregas atrás, valores Truthy y Falsy. En este caso, necesitamos que el valor de retorno sea Truthy para que el elemento pase la prueba. Todo elemento con valor será considerado Truthy y nuestro elemento undefined será Falsy.

Esto también se puede escribir como

const filtrado = newUsers3.filter(item => {
if(item) {
return true
}
return false
})
console.log({ filtrado })

La otra característica genial de estos métodos, es que son "concatenables", es decir, se pueden escribir uno tras otro. Ya que todos estos métodos retornan el nuevo arreglo, puedes aplicar directamente el siguiente método sin tener que crear nuevas variables.

const conTwitter = users.filter(item => item.twitter).map(item => {
return {
...item,
date: (new Date()).toLocaleDateString()
}
})
console.log({ conTwitter })

Aquí concatenamos (en orden inverso) un filtro y un map. Primero filtramos para obtener solo los items que si tienen la propiedad twitter y después hacemos un map sobre ese resultado (3 elementos) para aumentar las propiedades.

Array.reduce

Este es quizá el método madre de todos los demás métodos. Todos los métodos comentados pueden ser replicados utilizando este método que en general implementa un patrón que proviene de la programación funcional.

La definición oficial de este método en el sitio de mozilla MDN es:

El método reduce() aplica una función a un acumulador y a cada valor de una array (de izquierda a derecha) para reducirlo a un único valor

Los argumentos de reduce son:

  1. El valor actual.
  2. El valor anterior
  3. El índice actual.
  4. El arreglo de la que llamas a reduce.

Los dos primeros son los de gran relevancia.

Un ejemplo común de uso de reduce es: Obtener la suma de un arreglo.

¿Cómo obtienes la suma de los elementos de arreglo? (hazlo antes de mirar la respuesta y me comentas en Twitter cuál fue tu solución)

...

...

...

...

...

Algunas soluciones a este problema pueden ser:

// solución 1: forma mutable e imperativa
const array = [1,2,3,4,5,6,7,8,9]
// solución 1
let suma = 0
for(let i=0; i<array.length;i++){
suma+=array[i]
}
console.log({ suma })
// solución 2: forma inmutable e imperativa
let suma2 = 0
array.forEach(item => {
suma2+=item
})
console.log({ suma2 })

Pero con Array.reduce la solución inmutable y declarativa es:

// Solución 3: Inmutable y Declarativa
const suma3 = array.reduce((acumulador, actual) => {
return acumulador + actual
}, 0)
console.log({ suma3 })

Revisemos esta solución. En este caso la función reductora (nuestro callback) recibe dos argumentos que hemos llamado actual y acumulador. Además, el 2 argumento del método reduce es un entero, el número 0. Esto quiere decir.

  • Al iniciar la ejecución del método reduce y recorrer el arreglo, el valor de la variable acumulador es 0.
  • En cada iteración reduce ejecuta la función reductora que definimos y define que sus argumentos serán actuales que es el elemento de la actual iteración y acumulador que es el valor actual del acumulador, es decir, 0 en la primera ejecución.
  • En cada llamada a nuestra función reductora nosotros tomamos el valor de acumulador y lo sumamos con el valor de la variable actual y retornamos ese resultado.
  • El resultado retornado por nuestra función reductora es “pasado” como valor a acumulador. Es decir, acumulador tendrá el valor de la suma.

Ahora, revisemos esto, pero con un arreglo más complejo. Nuestro arreglo users.

Digamos que quiero conocer la cantidad de usuarios que tienen definido el atributo twitter.

Sin reducer podemos hacer:

// Contar usuarios sin atributo twitter
const haveTwitter = users.filter(item => item.twitter).length
console.log({ haveTwitter })

Primero filtramos y obtenemos un nuevo arreglo y luego “contamos” el tamaño de este usando Array.length.

También podemos hacer, con Array.reduce

// con reduce
const haveTwitter2 = users.reduce((acc, current) => {
if(current.twitter) {
return acc+1
}
return acc
}, 0)
console.log({ haveTwitter2 })

Solución que se ve más compleja pero que muestra que con Array.reduce puedes hacer muchas cosas.

En este caso lo que hacemos es revisar si el elemento actual tiene la propiedad twitter, en caso de que así sea sumamos 1 al acumulador acc, en caso contrario retornamos el acumulador como estaba.

Podemos simplificarlo un poco al hacer

const haveTwitter3 = users.reduce((acc, current) => {
return current.twitter ? (acc + 1) : acc
}, 0)
console.log({ haveTwitter3 })

Que es la misma solución anterior, pero usamos un operador ternario para simplificar el bloque condicional.

En resumen. Usarás reduce cada vez que necesites hacer transformaciones complejas de un arreglo, transformaciones que incluyan disminuir o "reducir" el arreglo.

Algunas cosas que tener en consideración cuando trabajas con reduce.

  • ¡No olvides el return !
  • No olvides definir el valor inicial (Que puede ser cualquier tipo de valor, incluyendo un objeto)

Veamos un ejemplo un poco más complejo de uso de reduce nuestro objetivo será dividir nuestro arreglo de usuarios en 2 arreglos, uno que tenga a todos los que tienen la propiedad twitter y otro para aquellos sin la propiedad.

¿Cómo obtendrías estos dos grupos usando reduce? Intenta hacerlo y si gustas publica tu respuesta en un gist y lo me mencionas en twitter 😉

Recuerda que reduce retorna un solo valor

...

...

...

...

Una posible solución sería

// Solucion a dividir el arreglo
const { withTwitter, withoutTwitter } = users.reduce((acc, current) => {
if(current.twitter){
acc.withTwitter.push(current)
}else{
acc.withoutTwitter.push(current)
}
return acc
}, { withTwitter: [], withoutTwitter: []})
console.log({ withTwitter, withoutTwitter})

En esta solución usamos el hecho de que reduce retorna un solo valor y que este valor retornado puede ser cualquier tipo de valor u objeto.

Entonces, definimos cuál será la forma del objeto resultado que queremos, en este caso un objeto que tiene dos propiedades { withTwitter, withoutTwitter }. Luego usamos esta definición para definir nuestro valor inicial.

El valor inicial utilizado aquí es un objeto con dos propiedades inicializadas como arreglos vacíos

{ withTwitter: [], withoutTwitter [] }

Y nuestra función reductora tiene una lógica muy simple.

  1. Si el elemento actual tiene la propiedad twitter, entonces agregamos el valor actual al acumulador en la propiedad withTwitter y cómo esta propiedad es un arreglo usamos Array.push.
  2. Si el elemento actual NO tiene la propiedad, hacemos lo mismo que el paso anterior, pero en el atributo withoutTwitter.
  3. Finalmente retornamos el acumulador acc.

Si quieres saber más sobre cómo funciona reduce bajo el capó, puedes revisar el Polyfill que mozilla ofrece que basa su implementación en un ciclo while.

Puedes encontrar el código usado en este artículo en este enlace.

Desafío

Dado un arreglo de objetos, cuyos elementos contienen otros arreglos. Usando Array.reduce ¿cómo puedes obtener un arreglo de una sola dimensión (es decir flattened) ?

Puedes usar el código base en este enlace

Créditos Extra**

  1. Tomar los datos del resultado anterior.
  2. Convertir las duraciones que están en segundos en horas.
  3. Sumar todas las duraciones.
  4. Piensa en un valor por hora y obtén el total de dinero obtenido por las horas de trabajo.

Si tienes dudas o preguntas respecto al desafío puedes comentarlas en mi repositorio AMA

¿Cómo se relaciona con React?

En React todos se trata de renderizar componentes, los componentes están ahí para mostrarnos ciertos datos en pantalla.

¿De dónde provienen esos datos? Pueden venir de al menos dos partes: Producidos por el usuario a través de formularios o eventos o desde el servidor.

¿Qué forma tienen estos datos? Múltiples formas o estructuras siendo muy común tener arreglos.

Entonces, ¿cual es la relación? React puede renderizar o desplegar cualquier tipo de “contenido" por medio de la prop children. Uno de esos tipos de datos es el arreglo. React puede renderizar una lista de componentes React dentro de otro, por lo que poder manipular arreglos para definir la estructura de datos que necesitamos para poder ser desplegada en la pantalla es esencial.

Un rápido ejemplo:

/* On React */
function ItemList({ data, owner }) {
return (
<ul>
{
data
.filter(item => item.owner === owner)
.map(item => (
<li key={item.id} > {item.name}</li>
))
}
</ul>
)
}

Conclusión

En resumen, manipular arreglos es parte del trabajo diario de un desarrollador Javascript, conocer diferentes métodos para esta tarea te ayudará a hacerlo de manera más eficiente.

De todos estos métodos hay al menos 3 que son poderosos y de uso constante.

  • Array.map
  • Array.filter
  • Array.reduce

Más recursos

Con estos métodos es posible realizar todo tipo de tareas más complejas como:

Cada uno de esos enlaces te llevará a una lección gratuita en egghead.io

Artículos Relacionados

¿Quieres mejorar tus habilidades de frontend?