Wextensible

Métodos para iterar por un Array

Figura
Figura. Un método para iterar por un Array nos permitirá consultar sus posiciones de forma consecutiva.

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.

arr.keys()

ES5

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()

ES5

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()

ES5

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])

ES5

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

Opciones
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

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
forforEachDiferencia
 
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

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.