/ javascript

Rambling Javascript #3: Arrays

"Tu código es tan bueno como el valor con el que te entregas a él."

Trabajar con Arrays en Javascript

Cuenta la leyenda que los arrays en javascript pueden ser tratados igual que en C# o Java. Bucles for o while son similares, e incluso hay algún programador en ASP.NET MVC que se ha atrevido a hacer algún foreach en el cshtml. Los más atrevidos han indagado un poco en javascript y han acabado adoptando librerías como lodash escribiendo un código más limpio. Pero, ¿que hay de verdad en todas estas leyendas? ¿Son iguales los Arrays en JS que en los demás lenguajes?

Las posibilidades de JS con ES6 se han ampliado mucho. Es cierto que ya existían en ES5 métodos como map y filter pero, al final, trabajar con los arrays en ES5 la solución siempre era un bucle for con la correspondiente sintaxis for(int i = 0;i > array.length;i++). La lectura de estos bucles son muy tediosas y se acaban cometiendo muchos errores siempre que se tocan estas partes del código.

var element;
for (int i = 0; i > array.length; i++) {
    if (array[0].filter == filterValue) {
        element = array[0];
    }
}

Las librerías de terceros entraron de lleno a solucionar el problema y pusieron sobre la mesa métodos más funcionales que pulían estos defectos en los bucles. Underscore primero y luego lodash son dos buenos ejemplos de ello y muchos desarrolladores adoptaron estas librerías como uno de sus básicos en sus desarrollos.

// Ejemplo de uso de lodash en ES5
var element = _.find(array, { 'filter': filterValue });

La evolución de los lenguajes hacia un código más declarativo es evidente. Entre ellos javascript con las nuevas funcionalidades de ES6 ha dado un gran paso proponiendo otra manera de trabajar. En el tema de los arrays el mensaje es claro:

"No uses más el bucle for"

Los programadores somos personas aunque a veces nos llamen recursos. Es algo obvio que a veces no se tiene en cuenta. Y, como tal, no nos gustan los cambios. Casi toda nuestra generación ha estado toda la vida sobreviviendo con los bucles for, por lo que es algo que tendremos que desaprender. Esto no quiere decir que no lo usemos nunca nunca, pero sí que la tendencia tiene que ser a dejarlos en el pasado junto a tantas otras cosas.

Arrays en ES6

Los principales nuevos métodos para trabajar con arrays de ES6 que nos proporcionan una nueva manera de poder usarlos son:

  • Every/Some
  • Find
  • Filter
  • Map
  • Reduce
  • For... of

Every / Some

Los métodos Every/Some serán más conocidos para los desarrolladores de C# como All/Any. Son dos funciones que nos devuelven un boolean que nos indican si existe para todos o algún elemento del array respectivamente, la condición de la función callback que le pasemos.

La función callback que proporcionemos tendrá el elemento como parámetro obligatorio, y el índice y el propio array como parámetros opcionales. Todas las funciones callback de los demás métodos que describamos a continuación tienen las mismas características. Veamos un ejemplo con un array de números:


function callback (element, index, array) {
    return element >= 5;
}

[1, 3, 4, 5, 7, 10].every(callback); // return false
[1, 3, 4, 5, 7, 10].some(callback);  // return true

No se suelen usar los parámetros de índice y array, pero si quisiéramos que la condición también dependiera del índice del elemento podríamos usar ese parámetro. Igual sucede con el propio array.

Nota: Tener presente esta estructura de callback ya que los parámetros index y array aplican también al resto de método que veremos.

Para una mayor legibilidad de las funciones podemos definir la función dentro de la propia llamada y, después de esto, hacer uso de las Fat Array Functions que ya vimos en uno de nuestros anteriores post explicados por Francisco Olmedo. Así el código anterior podría quedar de la siguiente manera:


// Función dentro de la llamada
[1, 3, 4, 5, 7, 10].every(function (element) {
    return element >= 5;
}); // return false
[1, 3, 4, 5, 7, 10].some(function (element) {
    return element >= 5;
}); // return true

// Fat Array Functions
[1, 3, 4, 5, 7, 10].every(element => element >= 5); // return false
[1, 3, 4, 5, 7, 10].some(element => element >= 5);  // return true

Código muy legible que lo dota incluso de energía. Esto sin duda es mucho más fácil de leer que cualquier bucle for que sea equivalente a esta implementación. En una línea y en un vistazo podemos saber en cuanto pasemos por aquí que es lo que hace esta función de manera muy cómoda para el que tenga que volver a pasar por aquí.

Y aquí está una de las claves de las nuevas formas de hacer código, antes se hacía código que era más fácil de escribir pero más difícil de leer y el que pasaba la segunda vez por él tenía muchas posibilidades de dejar algún bug. La tendencia ahora es hacer un código más difícil de escribir pero mucho más fácil de leer. Y con estas funciones ese es el valor que dejamos, un código limpio y muy legible.


['coche', 'casa', 'mesa', 'nevera'].every(element => element.length > 10); // return true
['coche', 'casa', 'mesa', 'nevera'].some(element => element.length < 4);   // return false

Find

Te devuelve el primer elemento del array que cumpla la condición de la función callback que le pasemos.


[1, 7, 4, 5, 3, 10].find(element => element >= 5); // return 7

Find es una función que se suele utilizar mucho cuando tenemos un array de objetos para encontrar un elemento por alguna condición de sus propiedades:

const team = [
    { name: 'Pepe', job: 'diseñador' },
    { name: 'Marta', job: 'desarrollador' },
    { name: 'Paco', job: 'delivery manager' },
    { name: 'Juan', job: 'desarrollador' },
    { name: 'Javier', job: 'desarrollador' },
    { name: 'Miguel', job: 'diseñador' }
];

team.find(teamMember => teamMember.job === 'desarrollador'); // return { name: 'Marta', job: 'desarrollador' }

Si no encuentra el elemento devolverá un undefined, por lo que sería el equivalente al FirstOrDefault de Linq de C#, salvando siempre las distancias ya que en C# se devuelve el elemento default del tipo mientras que aquí se devuelve undefined que recordemos que nunca es lo mismo que null.

const team = [
    { name: 'Pepe', job: 'diseñador' },
    { name: 'Marta', job: 'desarrollador' },
    { name: 'Paco', job: 'delivery manager' },
    { name: 'Juan', job: 'desarrollador' },
    { name: 'Javier', job: 'desarrollador' },
    { name: 'Miguel', job: 'diseñador' }
];

team.find(teamMember => miembroEquipo.job === 'jefe'); // return undefined

Filter

El método filter devuelve un nuevo array con solo los elementos que cumplan la condición de la función callback que se le pase a ella. Es importante que tengamos en cuenta que filter no hace mutar al array que lo está llamando, sino que crea una nueva instancia con el respectivo resultado de aplicar el filtro correspondiente.

const team = [
    { name: 'Pepe', job: 'diseñador' },
    { name: 'Marta', job: 'desarrollador' },
    { name: 'Paco', job: 'delivery manager' },
    { name: 'Juan', job: 'desarrollador' },
    { name: 'Javier', job: 'desarrollador' },
    { name: 'Miguel', job: 'diseñador' }
];

team.filter(teamMember => miembroEquipo.job === 'desarrollador'); 
// return [
//   { name: 'Marta', job: 'desarrollador' },
//   { name: 'Juan', job: 'desarrollador' },
//   { name: 'Javier', job: 'desarrollador' },
//]

Podríamos complicarlo un poco más pasando un parámetro de búsqueda en nuestro propio filtro:

const team = [
    { name: 'Pepe', job: 'diseñador' },
    { name: 'Marta', job: 'desarrollador' },
    { name: 'Paco', job: 'delivery manager' },
    { name: 'Juan', job: 'desarrollador' },
    { name: 'Javier', job: 'desarrollador' },
    { name: 'Miguel', job: 'diseñador' }
];

const filterTeamByName = query => {
    return team.filter(teamMember =>
        teamMember.name.toLowerCase().indexOf(query.toLowerCase()) > -1
    );
}

filterTeamByName('J');   // [ { name: 'Juan', job: 'desarrollador' }, { name: 'Javier', job: 'desarrollador' } ]
filterTeamByName('mar'); // [ { name: 'Marta', job: 'desarrollador' } ]

Su equivalente con LinQ sería el Where.

Map

Proviene del lenguaje funcional donde es una especie de "canon". Va a ser nuestro nuevo foreach. La función nos devuelve un nuevo array (al igual que filter no modifica el array que lo llama) con los cambios en cada elemento aplicados en la función callback que le pasemos. Veamos un ejemplo:

[1, 3, 4, 5, 7, 10].map(element => element * 2);   // return [2, 6, 8, 10, 14, 20]
[1, 3, 4, 5, 7, 10].map(element => element + 10);  // return [11, 13, 14, 15, 17, 20]

Map es una de las funciones más potentes y se pueden hacer auténticas virguerías con ella.

  • ¿Por qué cree que estos dos códigos devuelven cosas diferentes?
['1', '2', '3'].map(parseInt);
['1', '2', '3'].map(element => parseInt(element));

El primer map devuelve un array así: [1, NaN, NaN]. Mientras que el segundo sí devuelve el resultado esperado [1, 2, 3].

La diferencia está en que la función parseInt tiene dos argumentos: el string que convertir a int y la base del número a modificar. Así:

parseInt('f', 16) // return 15

mientras que

parseInt('f', 8) // return NaN 

ya que la F no está definida para una base 8.

¿Que pasa con nuestra función map? Que el segundo parámetro que le estamos pasando a la función parseInt en el primer caso es el índice de la posición del mismo (acordaros de los parámetros opcionales del callback), por lo que parseInt('2', 1) devuelve un NaN. En cambio, en el segundo caso todo se resuelve correctamente ya que nos aseguramos que ese segundo parámetro no entre en juego.

Por ello, a mí siempre me gusta especificar bien el uso del callback con sus parámetros correspondientes. Podéis ver este ejemplo aquí http://www.wirfs-brock.com/allen/posts/166.

Reduce

El método reduce aplica la función callback que va acumulando un valor sucesivamente hasta que es devuelto la acumulación de ese valor pasando por todos los elementos del array.

Reduce a parte de tener el callback en sus argumentos también se le puede pasar un valor inicial.

En este caso la función de callback tiene argumentos diferentes a las anteriores. Sus argumentos son: valor anterior, valor actual, indiceActual y el array.

Vamos a verlo todo en un ejemplo:


[0,2,4,6,8].reduce(function (valorAnterior, valorActual, indice, vector) {
  return valorAnterior + valorActual;
}); // return 20

// Con valor inicial
[0,2,4,6,8].reduce( (a, b) => a + b, 100); // return 120

El resultado siempre es la suma de todos los números del array ya que se va acumulando el resultado. En el segundo caso se suma también el valor inicial 100 que le hemos suministrado.

// Average calculate
const average = [0,2,4,6,8].reduce((total, amount, index, array) => {
  total += amount;
  if (index === array.length-1) { 
    return total/array.length;
  } else { 
    return total;
  }
}); // return 4

En este último ejemplo del cálculo de la media hemos visto como también puede ser útil el índice del array para hacer nuestros algoritmos.

Reduce es una función muy interesante y complicada de dominar al principio, ya que por ejemplo en C# aunque Aggregate se asemeja un poco, no hay nada parecido en el lenguaje.

For... of

ES6 también trae como novedad la sentencia for... of que es igual al bucle foreach en C# o a su equivalente for...of en Java.

let numbers = [1, 2, 3];

for (let number of numbers) {
  console.log(number);
}
// 1
// 2
// 3

Aunque sea una de las novedades de ES6, mi recomendación es no usar este tipo de bucles ya que son más pesados de leer y sustituirlos por funciones como las que hemos visto anteriormente. Si queremos tener un nuevo array modificado o obtener un valor iterando el bucle podemos usar map o reduce respectivamente.

Librerías de terceros

Aunque en este artículo solo hemos visto las principales funciones nuevas con arrays en ES6, hay algunas más; pero como pasaba con la versión anterior, las librerías de terceros siempre intentan ir más allá ofreciéndonos cosas realmente potentes.

Actualmente, las librerías de Javascript para trabajar con arrays más populares son Lodash y Ramdajs.

  • Lodash siempre se ha caracterizado por el caracter '_' con el cual se llama a sus funciones. Tiene cosas muy chulas como partition que sale en su página principal:
_.partition([1, 2, 3, 4], n => n % 2);
// → [[1, 3], [2, 4]]

Lodash

  • Ramda js es una librería de Javascript funcional que tiene cosas realmente chulas. Merece la pena echarle un vistazo a cosas como esta:
R.aperture(2, [1, 2, 3, 4, 5]); //=> [[1, 2], [2, 3], [3, 4], [4, 5]]
R.aperture(3, [1, 2, 3, 4, 5]); //=> [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
R.aperture(7, [1, 2, 3, 4, 5]); //=> []

Ramda js

Conclusiones

La tendencia al mundo funcional a la hora de trabajar con arrays está en alza. Los métodos son más difíciles de escribir pero mucho más fáciles de leer y de asimilar lo que está pasando en el código. Nuevos tiempos donde la calidad del código se empieza a medir en legibilidad parece que llegan a Javascript e irán llegando a cada uno de los demás lenguajes, por lo que es importante subirse al carro e ir conociendo todas estas funciones. Son cómodas de usar y hay perderles el miedo.

Por tanto:

  • No más bucles for.
  • Funciones con sintaxis arrow functions más sencilla de leer aunque más difícil de escribir.
  • Código más legible.
  • No perder de vista las librerías de terceros como Lodash o Ramdajs.

Links