Métodos para iterar por un Array en ES6
Métodos para iterar por un Array
En el primer tema de esta serie dedicamos algunos apartados para ver las distintas formas de iterar por un Array. En este tema vamos a exponer los métodos keys()
, values()
, entries()
y forEach()
. Algunas cosas ya se habrán expuesto allí, pero otras son nuevas. También en el tema sobre Symbol.iterator se comentan cosas relacionadas con los símbolos bien conocidos y la iteración en objetos.
Otros métodos también iteran por un Array, pero con un propósito específico. Por ejemplo map()
o filter()
iteran para devolver un nuevo Array. Otros que iteran son every()
o some()
, pero su propósito es saber si todos o alguno de los elementos cumplen con un test. Así que en ese tema sólo contemplamos aquellos métodos que sirven para iterar por un Array sin otra finalidad específica.
- Métodos para modificar el propio Array
- push, pop, shift y unshift para agregar y eliminar elementos. Ejemplos para implementar una pila y una cola.
- copyWithin para copiar y pegar un rango de elementos.
- fill para rellenar elementos con el mismo valor.
- reverse para invertirlo (
darle la vuelta
). - sort para ordenarlo. Ejemplo para ordenar una tabla.
- splice para eliminar elementos.
- Métodos para iterar por un Array
- Métodos para crear nuevos Array
- concat para concatenar dos Array en un nuevo Array.
- map para iterar por un Array ejecutando un callback y devolver un nuevo Array.
- slice para recortar un Array y devolverlo en un nuevo Array. Ejemplo de uso en algoritmo de búsqueda binaria.
- filter para iterar por un Array ejecutando un callback que filtra los elementos, devolviéndolos en un nuevo Array. Ejemplo para filtrar elementos del DOM.
- Métodos para buscar en un Array
- every comprueba que todos los elementos de un Array cumplen un callback.
- some comprueba que algún elemento de un Array cumple un callback.
- find encuentra un valor en un Array que cumpla un callback. Cómo obtener el valor seleccionado en elementos input tipo radio.
- findIndex encuentra el primer índice de un Array que cumpla un callback.
- includes comprueba si un Array tiene un determinado valor.
- indexOf busca el primer índice en un Array que tenga un determinado valor.
- lastIndexOf busca el último índice en un Array que tenga un determinado valor.
- Métodos que devuelven un valor transformado desde un Array
- join transforma un Array devolviendo un String concatenando sus elementos con el String del argumento.
- reduce transforma un Array en un valor final simple mediante un callback. Exponenciación rápida con el método reduce().
- reduceRight transforma un Array en un valor final simple mediante un callback, iterando desde el final del Array hacia el principio.
- toString transforma un Array en un String concatenando sus elementos con una coma.
- toLocaleString transforma un Array en un String concatenando sus elementos en formato local con una coma.
arr.keys()
El método keys()
devuelve un objeto iterador de Array que nos permite acceder a los índices que nos sirven para iterar por el Array. En el primer tema vimos el bucle For-of, un bucle donde ese objeto iterador puede entrar en juego. El método next()
del iterador nos va devolviendo los elementos del Array.
let arr = ["a", "b"]; let iterador = arr.keys(); console.log(typeof iterador); // "object" console.log(iterador); // ArrayIterator{} console.log(iterador.next()); // {value: 0, done: false} console.log(iterador.next()); // {value: 1, done: false} console.log(iterador.next()); // {value: undefined, done: true}
El objeto devuelto por next()
contiene el valor del elemento del Array y la variable done
que indica si ha llegado al final del Array. El método next()
está implícito en los bucles for..of
.
let arr = ["a", "b"]; for (let i of arr.keys()){ console.log(arr[i]); // "a" // "b" }
Un iterador de Array es un objeto, no un Array. Lo podemos convertir en un Array usando el operador de propagación.
let arr = ["a", "b"]; let iterador = arr.keys(); let indices = [...iterador]; // [0, 1]
arr.values()
El método values()
devuelve un iterador de Array con los valores de un Array.
let arr = ["a", "b"]; let iterador = arr.values(); for (let v of iterador){ console.log(v); // "a" // "b" }
El método values()
es el predeterminado de un Array, pues es el que se adjudica a su símbolo bien conocido [Symbol.iterator]
. De esta forma actuará cuando usemos el operador de propagación. En este ejemplo propagamos el primer Array dentro del segundo. JavaScript usará el método predeterminado para obtener los valores con values()
.
let arr = ["a", "b"]; console.log(arr[Symbol.iterator]); // function values() {..} let arr2 = [...arr, "c"]; console.log(arr2); // ["a", "b", "c"]
Este comportamiento lo podemos cambiar adjudicando otra función al [Symbol.iterator]
. En este caso le ponemos keys()
y observamos que propaga los índices del primer Array en lugar de sus valores:
let arr = ["a", "b"]; arr[Symbol.iterator] = Array.prototype.keys; console.log(arr[Symbol.iterator]); // function keys {..} let arr2 = [...arr, "c"]; console.log(arr2); // [0, 1, "c"]
Incluso podemos crearnos nuestro propio iterador. En este ejemplo usamos una función generadora que convierte a mayúsculas los valores del Array.
let arr = ["abc", "def"]; arr[Symbol.iterator] = function* (){ let i = 0; while (i<this.length){ yield this[i++].toUpperCase(); } }; let arr2 = [...arr, "ghi"]; console.log(arr2); // ["ABC", "DEF", "ghi"]
Estos métodos de Array pueden generalizarse a objetos array-like, como un String, usando call()
o apply()
. Aunque lo mismo podría conseguirse propagando el String en un Array. O incluso aplicándo directmente el for..of
sobre el String, porque un String también es iterable:
let str = "ab"; //Generalizando values() a un String for (let v of [].values.call(str)) { console.log(v); // "a" // "b" } //Lo mismo se consigue propando el String en un Array for (let v of [...str]) { console.log(v); // "a" // "b" } //Y aún más fácil, el propio String es iterable for (let v of str) { console.log(v); // "a" // "b" }
arr.entries()
El método entries()
devuelve un iterador de Array con parejas índice y valor que nos permiten iterar por el Array:
let arr = ["a", "b"]; //Propagando el iterador vemos que es un Array de parejas índices-valor console.log([...arr.entries()]); // [[0, "a"], [0: "b"]] for (let [i, v] of arr.entries()){ console.log(`arr[${i}] = ${v}`); // arr[0] = "a" // arr[1] = "b" }
En el código anterior usamos destructuring para declarar las parejas índice-valor.
arr.forEach(callback[, thisArg])
El método forEach()
itera por los elementos de un Array ejecutando un callback (una función) por cada elemento. El método siempre devuelve undefined
, como se observa en este ejemplo donde encadenamos los elementos de un Array en una variable global cad
.
function callback(valor, indice){ cad += `${indice}=${valor}, `; } let cad = ""; let dev = ["a", "b"].forEach(callback); console.log(dev); // undefined console.log(cad); // "0=a, 1=b, "
El tercer argumento del callback es el propio Array sobre el que estamos iterando. En este ejemplo usamos una expresión de función, pasando en el argumento thisArg del método también el propio Array. Vemos que dentro del callback podemos acceder al Array de tres formas:
let arr = ["a"]; arr.forEach(function(valor, indice, array) { console.log(array); // ["a"] del argumento array de callback() console.log(arr); // ["a"] de arr que es global console.log(this); // ["a"] del argumento thisArg de forEach() }, arr);
Si embargo si usamos una función flecha el thisArg será ignorado y this
apuntará a Window
. Aunque en modo estricto this
siempre será undefined.
let arr = ["a"]; arr.forEach((valor, indice, array) => { console.log(array); // ["a"] del argumento array de callback() console.log(arr); // ["a"] de arr que es global console.log(this); // Window {..} (undefined en modo estricto) }, arr);
Un bucle forEach
no puede romperse con break
como si hacemos en bucles normales. Para forzar la salida en un forEach
habría que lanzar un error con throw
para detener la ejecución del código. Aunque eso funcione resulta un poco forzado.
//Bucle for con un break let arr = [12, 44, 125, 188]; let cad = ""; for (let valor of arr){ if (valor > 100){ break; } else { cad += valor + ", "; } } console.log(cad); // "12, 44, " //Bucle forEach con un throw cad = ""; try { arr.forEach((valor) => { if (valor > 100){ throw "Sale de ForEach"; } else { cad += valor + ", "; } }); } catch(e){ console.log(e); // "Sale de ForEach" } console.log(cad); // "12, 44, "
El método forEach()
puede aplicarse a los array-like, como a un String. En este ejemplo lo usamos para invertir una cadena por medio del método call()
. Recuerde que [].forEach.call()
hace lo mismo que Array.prototype.forEach.call()
, pero es más corto:
let str = "abcdefg"; let str2 = ""; [].forEach.call(str, v => str2 = v + str2); console.log(str2); // "gfedcba"
Podemos prescindir de call()
(o apply()
en su caso) si usamos el operador de propagación. En este ejemplo convertimos el String en un Array sobre el que podemos aplicar directamente el método forEach
:
let str = "abcdefg"; let str2 = ""; [...str].forEach(v => str2 = v + str2); console.log(str2); // "gfedcba"
El método forEach()
se podría usar con los HTMLCollection que son array-like devueltos por los métodos del DOM como getElementsByName()
y similares. En el siguiente ejemplo iteramos por elementos <input type="radio">
para adjudicarles eventos click, tal que al pulsarlos nos vierta su atributo value
en otro elemento del DOM.
<fieldset><legend>Opciones</legend> <label>Opción 1 <input type="radio" name="radio-foreach" value="1" /></label> <label>Opción 2 <input type="radio" name="radio-foreach" value="2" /></label> <label>Opción 3 <input type="radio" name="radio-foreach" value="3" /></label> <label>Opción 4 <input type="radio" name="radio-foreach" value="4" /></label> </fieldset> <div id="resultado-foreach"></div> <script> [...document.getElementsByName("radio-foreach")].forEach(radio => { radio.addEventListener("click", (event) => { document.getElementById("resultado-foreach").innerHTML = `Ha hecho <span class="big">click</span> sobre el elemento con <code>value = <span class="azul"> ${event.target.value}</span></code>`; }, false); }); </script>
En el siguiente ejemplo interactivo puede ver lo anterior en ejecución:
Ejemplo: Ejemplo de uso de forEach() para adjudicar eventos DOM
Lo anterior podría ser una buena tarea para un forEach()
pues se trata de iterar por los elementos haciendo algo en cada uno y sin necesidad de tener que cortar el bucle con break
. Pero no hay que olvidar que un simple bucle for
podría hacer lo mismo:
let radios = document.getElementsByName("radio-foreach"); for (radio = 0; radio<radios.length; radio++) { radio.addEventListener("click", () => { //...hacer algo aquí }, false); });
Los bucles for
y for of
pueden resultar en un código más claro que usando un forEach
:
//Bucle for let arr = [1, 2, 3]; for (let indice=0; indice<arr.length; indice++){ arr[indice] += 10; } console.log(arr); // [11, 12, 13] //Bucle for of + keys() for (let indice of arr.keys()){ arr[indice] += 100; } console.log(arr); // [111, 112, 113] //Bucle for of + entries() for (let [indice, valor] of arr.entries()){ arr[indice] = valor + 1000; } console.log(arr); // [1111, 1112, 1113] //Bucle forEach arr.forEach((valor, indice, array) => array[indice] = valor + 10000); console.log(arr); // [11111, 11112, 11113]
¿Y que pasa con la eficiencia? En este ejemplo interactivo vamos a enfrentar un for
con un forEach
y ver cuál es más eficiente. En cada iteración no hacemos nada para valorar exclusivamente el coste de iterar:
Ejemplo: Eficiencia bucles for y forEach
Durante ese tiempo ejecutaremos un bucle for
:
let arr = [0,1,2,3,4,5,6,7,8,9]; for (let i = 0; i<arr.length; i++){ //...no hacemos nada }
Y un bucle forEach
:
let arr = [0,1,2,3,4,5,6,7,8,9]; arr.forEach(i => { //...no hacemos nada });
Cuantas más iteraciones hagamos más eficiente será el proceso
Iteraciones (Cuanto mayor mejor) | % mejora for s/ forEach | ||
---|---|---|---|
for | forEach | Diferencia | |
Podríamos pensar que el forEach
debe ser menos eficiente, pues en cada iteración hay un función que ejecutar. Pero en Chrome 51 el for
es sólo un 20% más eficiente que un forEach
, mientras que en Firefox 46 no hay diferencias significativas entre ambos bucles.