Un Array es un vector en JavaScript

Figura
Figura. El Array en JavaScript implementa un vector.

Un vector en programación informática es una lista de elementos, cada uno con una posición o índice que sirve para acceder a sus elementos. Vector, formación o arreglo son términos que se usan como traducción del inglés array. JavaScript utiliza la estructura de datos denominada Array para implementar los vectores. El Array en JavaScript es una de las estructuras de datos con más uso. Disponen de muchos métodos y se aplican en variadas situaciones, por lo que debemos entenderlas para sacarles el mayor provecho.

Un Array se indexa desde cero en JavaScript. Esto es algo que viene desde las primeras implementaciones de los vectores en programación. Se trataba de que cada índice fuera una posición de memoria consecutiva. Así cuando se declaraba un vector se hacía apuntando una variable a una determinada dirección base (b) de memoria. Esa era la posición cero del vector. Para ir a una posición o índice determinado se utilizaba el concepto de desplazamiento (i), así que la ejecución del programa sólo tenía que sumarlo a la dirección base (b+i) y obtenía la dirección de memoria adecuada.

La forma más simple de crear un Array es mediante notación literal. Por ejemplo, [1, 2, 3] sería un literal de Array con números enteros. Accedemos a un elemento usando la variable junto al índice entre corchetes, algo como arr[0] para leer o modificar el elemento del primer índice. La propiedad length guarda el total de elementos que hay en el Array. Dado que se indexan desde cero, el último elemento tiene un índice length-1.

En un Array podemos incluir elementos de tipos diferentes. De hecho podemos ponerle cualquier expresión que devuelva un valor, como el comparativo del tercer elemento o la ejecución de una función del último elemento en el código siguiente:

function fun(x){return x+1;}
let arr = [123, "abc", (5 > 4), {a: 99}, fun(1)];
console.log(arr[0]); // 123
console.log(arr[1]); // "abc"
console.log(arr[2]); // true
console.log(arr[3]); // Object {a: 99}
console.log(arr[4]); // 2
//Es lo mismo usar un String que un número entero 
//no negativo en el índice cuando accedamos
console.log(arr["4"]); // 2
//Longitud del Array
console.log(arr.length); // 5
//Tipo de objeto
console.log(typeof arr); // "object"
console.log(arr.constructor.name); // "Array"
console.log(Object.prototype.toString.call(arr)); // "[object Array]"
    
Todos los códigos expuestos en este tema han sido probados en Chrome 51, de donde se obtienen las salidas por la consola.

Los índices son números enteros no negativos. Para acceder a un elemento es indiferente hacer arr[4] que arr["4"]. De hecho cualquier expresión que devuelva un número entero no negativo o su String

Se observa que typeof nos devuelve el tipo object. Esto es porque un Array no es un tipo primitivo de datos como string, number, boolean, symbol, null o undefined. Para saber si una variable contiene un Array podemos usar el nombre del constructor en ES6, o el método toString() del prototipo de Object que nos devuelve "[object Array]". Si quiere saber más sobre tipos de datos puede consultar el tema Símbolo toStringTag, donde se expone una tabla de tipos JavaScript.

En JavaScript los Arrays son unidimensionales. Pero dado que podemos incluir elementos Array es posible construir matrices con el sentido de Arrays multidimensionales. Observe como accedemos a un elemento con los índices entre corchetes para cada dimensión del Array en esta matriz de dos dimensiones y con 3×3 elementos:

let arr = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];
console.log(arr[0][0], arr[0][1], arr[0][2]); // 1 2 3
console.log(arr[1][0], arr[1][1], arr[1][2]); // 4 5 6
console.log(arr[2][0], arr[2][1], arr[2][2]); // 7 8 9
    

Agregando elementos a un Array

Podemos agregar nuevos elementos a un Array con la notación arr[arr.length] = valor agregándose al final. Recuerde que al contar desde cero la última posición será length-1, por lo que la posición length corresponderá a un nuevo elemento agregado al final. Pero resulta más simple usar el método push(). Simplemente con arr.push(valor) agregamos un nuevo elemento al Array.

let arr = ["a"];
arr.push("b"); // Agregando con el método push()
arr[arr.length] = "c"; // o con notación corchetes
console.log(arr); // ["a", "b", "c"];
console.log(arr.length); // 3
    
Figura
Figura. Un Array que le falta un elemento.

No es nada recomendable, pero si usa un índice concreto para agregar un elemento al final del Array ese índice debería ser el siguiente al último. Si continuando con el código anterior, cuyo último índice fue 2 (uno menos que la longitud), y ahora hacemos arr[4] = "d" saltándonos el índice 3 sucederá lo que se observa en la Figura, que no tiene ese índice. Podemos decir que hay un hueco en las posiciones del Array.

Los elementos se almacenan con parejas índice y valor. Vemos que sólo hay cuatro elementos, pero el valor de length es cinco. El elemento con índice 3 faltante será recuperado con valor undefined. La longitud length se ajustará a uno más el índice más alto que exista:

arr[4] = "d";
console.log(arr); // Chrome:  ["a", "b", "c", 4: "d"]
                  // Firefox: [ "a", "b", "c", <1 ranura vacía>, "d" ]
console.log(arr.length); // 5
console.log(arr[3]); // undefined
console.log(typeof arr[3]); // "undefined"
    

Observe como en la consola de Chrome se indica 4: "d" cuando se produce una ausencia del anterior índice. Firefox es más claro en este sentido, pues nos dice que hay 1 ranura vacía (traducción de Firefox que opino poco acertada, pues yo preferiría 1 hueco).

Los huecos en un Array pueden producirse por la asignación a una posición por encima de length y también por otros motivos, como el uso del operador delete para eliminar un elemento. O simplemente porque declaramos literalmente ese hueco con algo como let arr = [1,,3]. En el siguiente código creamos estos tres huecos.

//Un Array con un hueco en el índice 1
let arr = [1, , 3, undefined, null];
console.log(arr); // [1, , 3, undefined, null]
console.log(arr[1]); // undefined
console.log(arr.length); // 5
//Borramos el elemento del índice 2
delete arr[2];
console.log(arr); // [1, , , undefined, null]
console.log(arr[2]); // undefined
console.log(arr.length); // 5 (length no varía)
//Agregamos un elemento por encima de length
arr[6] = 7;
console.log(arr); // [1, , , undefined, null, , 7]
console.log(arr.length); // 7 (incrementó 2)
    

Vea que no es lo mismo un hueco que un valor undefined o null. Un hueco es que no existe una propiedad con el índice que ocupa esa posición en el Array. Aunque cuando intentamos recuperar el valor en ese índice JavaScript nos devuelva undefined. Las técnicas para iterar por un Array que devuelven sólo elementos iterables no ignoran los huecos. Pero otras si los ignoran, como veremos en el siguiente apartado.

Un Array es en el fondo un objeto. Agregando claves que no sean enteros no negativos hará que se agregue una propiedad con esa clave. Pero la longitud no variará, seguirá siendo uno más la clave con entero no negativo más alta:

let arr = ["a"];
arr["key"] = "cadena";
arr.clave = "valor";
console.log(arr); // Chrome: ["a", key: "cadena", clave: "valor"]
                  // Firefox: ["a"]
console.log(arr.length); // 1
console.log(arr.key); // "cadena"
console.log(arr.clave); // "valor"
    

De hecho cualquier otra cosa que no sea un número entero no negativo será agregado como una propiedad del Array y no como un índice. En este ejemplo usamos como nombre de propiedades los números -1 y 2.5:

let arr = ["a"];
//Agregamos propiedades, NO SERÁN ELEMENTOS DEL ARRAY
arr[-1] = "b";
arr[2.5] = "c";
console.log(arr); // ["a", -1: "b", 2.5: "c"]
//La longitud del Array no varía
console.log(arr.length); // 1
//Estas claves numéricas no serán índices del Array
console.log(arr[-1]); "b"
console.log(arr[2.5]); "c"
    

Ver más sobre los nombres de propiedades de un objeto.

Eliminando elementos en un Array

Antes vimos que usar delete para eliminar un elemento de un Array dejará un hueco. En este código eliminamos el tercer elemento observándose en la consola de Firefox como queda una ranura vacía, permaneciendo igual la longitud.

let arr = ["a", "b", "c"];
delete arr[2];
console.log(arr); // [ "a", "b", <1 ranura vacía> ]
console.log(arr.length); // 3
    

Por lo tanto delete no es la forma adecuada para eliminar un elemento de un Array. El método arr.splice(pos, num) es uno de los que modifican el propio Array, siendo indicado para eliminar elementos, empezando en el índice pos y eliminando num elementos. Haríamos lo siguiente para eliminar el último elemento:

let arr = ["a", "b", "c"];
arr.splice(2, 1);
console.log(arr); // ["a", "b"]
console.log(arr.length); // 2
    

En JavaScript hay que tener cuidado cuando creamos una referencia a otra variable, es decir, un alias. En el siguiente código tenemos la variable let x = 1 y luego declaramos un alias let y = x que apunta a la variable anterior. Las dos apuntan al mismo valor. Pero si reasignamos una de ellas la otra no se reasigna también, sino que sigue apuntando al valor inicial.

let x = 1;
let y = x;
console.log(x, y); // 1 1
//Reasignamos la x
x = 2;
//Pero la y sigue con el mismo valor
console.log(x, y); // 2 1
    

Por lo tanto si hay alias a elementos de un Array, cualquier cambio en el valor de un elemento como reasignaciones o eliminaciones no se verán repercutidas en los alias:

let arr = ["a", "b", "c"];
//Alias al segundo elemento "b"
let x1 = arr[1];
//Modificamos el segundo elemento "b"
arr[1] = 999;
console.log(arr); // ["a", 999, "c"]
//El alias no se modifica
console.log(x1);  // "b"
//Alias al tercer elemento "c"
let x2 = arr[2];
//Eliminamos el tercer elemeto
arr.splice(2, 1);
//El Array ahora tiene 2 elementos
console.log(arr); // ["a", 999]
//Pero el alias x sigue con el mismo valor
console.log(x2); // "c"    
    

También es importante el tema de vaciar un Array. Se suele hacer con arr = [], pero esto supone una reasignación, por lo que un alias al propio Array no será actualizado:

let arr = [1, 2, 3];
//Alias al Array
let x = arr;
//Alias a un elemento del Array
let y = arr[0];
//Reasignamos con Array vacío
arr = [];
//Los alias no resultan afectados
console.log(x); // [1, 2, 3]
console.log(y); // 1
    

Realmente en lo anterior no estamos vaciando el Array, sino reasignándolo a uno nuevo vacío. Para vaciar efectivamente un Array, o incluso para disminuir su tamaño, podemos hacer uso de lo que dice la especificación ECMA262 6th Edition: 22.1.4 Properties of Array Instances: Reducir el valor de la propiedad length tiene el efecto secundario de eliminar los elementos del array cuyos índices estén entre el valor anterior de length y el nuevo valor. En definitiva, que reducir la longitud supone que se eliminen efectivamente los elementos del Array.

En el siguiente ejemplo vemos como al actuar sobre length se eliminan los elementos no suponiendo una nueva reasignación del Array. Variable y alias ahora se actualizan a lo mismo, siendo entonces una mejor forma de vaciar un Array. En cualquier caso los alias a los elementos se mantienen.

let arr = [1, 2, 3];
let x = arr;
let y = arr[0];
//Lo reducimos
arr.length = 2;
console.log(arr);  // [1, 2]
console.log(x); // [1, 2]
console.log(y); // 1
//Lo vacíamos completamente
arr.length = 0;
console.log(arr); // []
console.log(x); // []
console.log(y); // 1
    

Otra forma de vaciar un Array es usando arr.splice(0), donde no indicamos el segundo argumento para el número de elementos a eliminar, en cuyo caso se eliminan todos. Al igual que antes, variable y alias se vacían, pero no los alias a los elementos.

let arr = [1, 2, 3];
let x = arr;
let y = arr[0];
arr.splice(0);
console.log(arr); // []
console.log(x); // []
console.log(y); // 1
    

En el siguiente ejemplo probamos la eficiencia estas tres formas de vaciar un Array:

Ejemplo: Eficiencia de varias técnicas para vaciar un Array

Durante ese tiempo creamos el Array

let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
        

y lo vaciaremos con arr = [], arr.length = 0 y arr.splice(0)

Iteraciones (Cuanto mayor mejor)% mejora []
sobre length=0
% mejora []
sobre splice(0)
Con arr = []Con arr.length=0Con arr.splice(0)
 
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

En las condiciones que estoy haciendo la prueba se observa algo de mejora al reasignar un Array vacío frente a anular su longitud. Con una prueba de 1000 ms observo mejoras en torno a 10% en Chrome 51 y 15% en Firefox 46. La opción de arr.splice(0) obtiene peores resultados, con un 20% en Chrome y en torno al 80% en Firefox. Sin embargo la eficiencia podría depender del número y tipo de elementos que albergue el Array.

Iterar por un Array

La forma más sencilla de iterar por un Array es usando un bucle for donde controlamos un índice en el rango desde cero hasta el total de elementos del Array. Pero con el nuevo bucle for of de ES6 las cosas son ahora más sencillas. En el siguiente cuadro se reúnen los métodos principales para iterar por un Array. La mayor parte de ellos se explican con los correspondientes métodos. Dos de ellos con los métodos forEach() y map() serán comentados en los temas siguientes.

En la muestra de códigos la i es la variable que contiene el índice de un elemento del Array y la v el valor de ese elemento. La columna huecos se refiere a posiciones de un Array que no existen, tal como explicamos en el apartado anterior. No todas las técnicas devuelven los huecos.

Técnicapara recuperar propiedades
Sólo
iterables
Sólo
símbolos
No enu-
merables
Del pro-
totipo
Huecos
Bucle for
for(let i=0; i<arr.length; i++){
    // v = arr[i]
}
        
Bucles for of
for (let v of arr){
    // v
}
        
for (let v of arr.values()){
    // v
}
        
for (let i of arr.keys()) {
    // v = arr[i]
}
        
for (let [i, v] of arr.entries()){
    // i, v
}
        
Métodos del prototipo
arr.forEach((v, i) => {
    // i, v;
});
        
nuevoArr = arr.map((v, i) => {
    // i, v;
});
        
Bucles for in
for(let i in arr){
    // v = arr[i]
}
        
for(let i in arr){
    if (arr.hasOwnProperty(i)){
        // v = arr[i]
    }
}
        
Métodos de Object
let keys = Object.keys(arr);
for (let i of keys){
    // v = arr[i];
}
        
let keys = 
Object.getOwnPropertyNames(arr);
for (let i of keys){
    // v = arr[i];
}
        
let keys = 
Object.getOwnPropertySymbols(arr);
for (let i of keys){
    // v = arr[i];
}
        
let pares = Object.entries(arr);
for (let [i, v] of pares){
    // i, v
}
        

Para iterar por un Array normalmente usaremos bucles for, for of o los métodos del prototipo forEach(), map() y otros. Pues lo que nos interesa son sólo los elementos que hay en los índices numéricos. Pero para ir entendiendo otros métodos que aplican a cualquier objeto se exponen los bucles for in y los métodos de Object.

A continuación se expone un ejemplo interactivo para ver el funcionamiento de las formas de iterar de la tabla anterior. Los navegadores actuales Chrome 51 y Firefox 46 no soportan Object.entries() mostrándose un error. El resto si deben funcionar en esos navegadores.

Ejemplo: Iterar por un Array

Bucles for of
Métodos de Array
Bucle for in
Bucle for of + métodos de Object

Array arr:

Resultado en la variable str tras iterar por ese Array:

CÓDIGO: Agregamos una propiedad enProto al prototipo, construimos un Array sobre el que iterar usando los títulos anteriores separados por espacios y anteponiéndole "Bucle x ", le borramos el segundo elemento con delete(arr[1]), y finalmente agregamos más propiedades a la instancia: una enumerable, otra no enumerable y una con un Símbolo.

Para iterar por ese Array usamos el código siguiente (i es el índice y v el valor del elemento en ese índice):

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

Iterar por un Array con un bucle For

La forma más conocida de iterar por un array es con un bucle for. Como estamos seguros de que la propiedad length siempre será la del último índice más uno, no nos saldremos de la iteración controlando que el índice de iteración sea menor que ese length:

let arr = ["a", "b"];
arr["key"] = "xyz"; // Claves no numéricas no se recuperan
//Pero si las numéricas no enumerables
Object.defineProperty(arr, 2, {value: "c", enumerable: false});
//BUCLE FOR
let cad = "";
for (let i=0, maxi=arr.length; i<maxi; i++){
    cad += (cad?", ":"") + arr[i];
}
console.log(cad); // "a, b, c"
    

Veáse que las propiedades no numéricas del objeto Array, como la agregada "key", no son capturadas por el bucle. Pero propiedades agregadas con defineProperty() como no enumerables, siempre que sean numéricas, si son capturadas. Otro detalle a tener en cuenta es el cacheado de la longitud del Array. Extrayendo maxi = arr.length antes de empezar a iterar evitamos tener que estar consultando la longitud en cada iteración, lo que supuestamente supone un ahorro de tiempo. Veamos si es cierto con el siguiente ejemplo.

while (new Date() - ini < tiempo){
    iteraciones++;
    if (tipo == 0) { // Cacheado
        let cad = "";
        for (let i=0, maxi=arr.length; i<maxi; i++){
            cad += (cad?", ":"") + arr[i];
        }
    } else if (tipo == 1) { // No cacheado
        let cad = "";
        for (let i=0; i<arr.length; i++){
            cad += (cad?", ":"") + arr[i];
        }
    } else { // Al revés, contando hacia abajo
        let cad = "";
        for (let i=arr.length-1; i>-1; i--){
            cad += (cad?", ":"") + arr[i];
        }                
    }
}
    

Ejemplo: Eficiencia de cachear longitud del Array antes de iterar

ms (de 100 a 3.000 ms)
elementos de un único carácter "a" (de 100 a 100.000)

Durante ese tiempo ejecutaremos un bucle con y sin cacheado de longitud.

Iteraciones (Cuanto mayor mejor)% mejora
al cachear
Iteraciones
al revés
CacheadoNo cacheadoDiferencia
 
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

En los navegadores actuales no se observan diferencias apreciables cacheando o no la longitud, por lo que no hay motivo actualmente para almacenar la longitud antes de empezar el bucle. Quizás una razón para hacerlo es por si el Array fuese modificado por error en el transcurso del bucle. En este código vamos modificando el Array agregando un elemento en cada iteración. Si no tuviésemos el condicional que rompe el bucle, éste seguiría indefinidamente. (Vea que break es la forma de salir de los bucles for o while).

let arr = [-1];
for (let i=0; i<arr.length; i++){
    //Agregamos un nuevo elemento
    arr.push(i);
    //Controlamos para que finalice pues en otro
    //caso seguiría indefinidamente
    if (i>100) break;
}
console.log(arr); // [-1, 0, 1, 2, 3, ..., 100]
    

Pero si cacheamos la longitud el bucle sólo hace una iteración:

let arr = [-1];
//Cacheamos la longitud
for (let i=0, maxi=arr.length; i<maxi; i++){
    //Agregamos un nuevo elemento
    arr.push(i);
}
//Sólo agrega un elemento y sale
console.log(arr); // [-1, 0]
    

En cualquier caso parece poco seguro modificar un Array si estamos iterando por él.

Iterar por un Array con un bucle For-in

Un bucle for como vimos antes nos sirve para iterar por un Array. Pero también nos serviría para iterar por un objeto siempre y cuando sus claves sean números enteros consecutivos que respondan adecuadamente a una sentencia como for(let i=n; i<=m; n++). Con eso estamos especificando un rango n..m con el que recorremos los elementos de ese objeto. Por ejemplo:

let obj = {10: "a", 11: "b", 12: "c", key: "d"};
let cad = "";
for (let i=10; i<=12; i++){
    cad += ((cad)?", ":"") + obj[i];
}
console.log(cad); // "a, b, c"
    

Observe como la clave key no es recuperada. En definitiva, un bucle for nos sirve para mover una variable en un rango de valores. Lo que luego hagamos con esa variable dependerá de la finalidad del bucle.

Pero el bucle for in tiene una finalidad específica. Se usar para extraer lo que contiene cualquier objeto, extrayéndose todas las propiedades enumerables, sean o no numéricas:

let obj = {10: "a", 11: "b", 12: "c", key: "d"};
let cad = "";
for (let i in obj){
    cad += ((cad)?", ":"") + obj[i];
}
console.log(cad); // "a, b, c, d"
    

Como un Array es también un objeto, ese bucle extrae todo lo que hay en su interior, incluso las propiedades que pudieran agregarse a la instancia de un Array:

let arr = ["a", "b"];
arr.key = "xyz";
//BUCLE FOR-IN
let cad = "";
for (let index in arr){
    cad += (cad?", ":"") + arr[index];
}
console.log(cad); // "a, b, xyz"
    

Sabemos que todo Array tiene la propiedad length. No sale en el bucle for in porque es una propiedad no enumerable. Vea como en el siguiente código agregamos otra propiedad key2 y la hacemos no enumerable usando el método defineProperty(), no siendo capturada por el bucle for in:

let arr = ["a", "b"];
arr.key = "xyz";
Object.defineProperty(arr, "key2", {enumerable: false, value: 123});
console.log(arr); // ["a", "b", key: "xyz", key2: 123]
//BUCLE FOR-IN
let cad = "";
for (let index in arr){
    cad += (cad?", ":"") + arr[index];
}
console.log(cad); // "a, b, xyz"
console.log(arr.propertyIsEnumerable("key")); // true
console.log(arr.propertyIsEnumerable("key2")); // false
console.log(arr.propertyIsEnumerable("length")); // false
    

El método propertyIsEnumerable() nos permite consultar si una propiedad es enumerable. El bucle for in extrae todas las propiedades enumerables, incluyendo las del prototipo. En el siguiente ejemplo agregamos una propiedad x al prototipo de Array para luego construir una instancia. Otra segunda propiedad y es agregada a esa instancia. El bucle nos devolverá los elementos indexados en el Array y las dos propiedades que hemos agregado. Pero si filtramos con hasOwnProperty(index) descartaremos las propiedades enumerables agregadas en el prototipo:

Array.prototype.x = "X";
let arr = [1, 2];
arr.y = "Y";
let cad = "";
for (let index in arr){
    //if (arr.hasOwnProperty(index)){
        cad += (cad?", ":"") + index + ': ' + arr[index];
    //}
}
console.log(cad);
//Sin hasOwnProperty obtenemos
    // "0: 1, 1: 2, y: Y, x: X"
//Con hasOwnProperty obtenemos
    // "0: 1, 1: 2, y: Y"
    

Vemos que el bucle for in no es apropiado para iterar por un Array, dado que sólo estamos interesados en sus propiedades numéricas enteras no negativas, el resto debería ignorarse en la mayor parte de los casos.

Iterar por un Array mediante una colección de claves obtenidas con métodos estáticos de Object

No parece práctico usar métodos de Object para aplicarlos a un Array, pues este constructor tiene los mismos método particularizados. Pero este apartado pretende exponer que un Array, como todos los objetos de JavaScript, heredan de Object. Entender esto es de suma importancia para saber como funciona JavaScript.

Para evitar usar hasOwnProperty en un for in podemos usar el método Object.keys(arr). Nos devuelve un Array con las claves enumerables de un objeto, no incluyendo las del prototipo.

Array.prototype.x = "X";
let arr = [1, 2];
arr.y = "Y";
//Object.keys() nos da un Array con claves de 
//propiedades enumerables propias
let keys = Object.keys(arr);
console.log(keys); // ["0", "1", "y"]
//Ahora iteramos con un bucle for clásico 
let cad = "";
for (let i=0; i<keys.length; i++){
    cad += (cad?", ":"") + keys[i] + ': ' + arr[keys[i]];
}
console.log(cad); // "0: 1, 1: 2, y: Y"
    

Si queremos obtener todas las claves enumerables y no enumerables de la instancia, excluyendo las del prototipo, podemos usar Object.getOwnPropertyNames(). En el siguiente código se recupera la no enumerable length y la agregada z también como no enumerable. Sin embargo no recupera las del prototipo como la x:

Array.prototype.x = "X";
let arr = [1, 2];
arr.y = "Y";
Object.defineProperty(arr, "z", {value: "Z", enumerable: false});
//Object.getOwnPropertyNames nos da un Array con claves de 
//propiedades enumerables y no enumerables propias (no del prototipo)
let keys = Object.getOwnPropertyNames(arr);
console.log(keys); // ["0", "1", "length", "y", "z"]
//Ahora iteramos con un bucle for clásico 
let cad = "";
for (let i=0; i<keys.length; i++){
    cad += (cad?", ":"") + keys[i] + ': ' + arr[keys[i]];
}
console.log(cad); // "0: 1, 1: 2, length: 2, y: Y, z: Z"
    

Los nuevos símbolos de ES6 que se agregan como claves de un objeto no pueden ser recuperados con los métodos existentes. El nuevo método Object.getOwnPropertySymbols() es similar al anterior, recuperando todos los símbolos enumerables y no enumerables propios de un objeto, no de su prototipo:

let arr = [1, 2];
Object.defineProperty(arr, Symbol("s"), 
    {value: "S", enumerable: true});
Object.defineProperty(arr, Symbol("t"), 
    {value: "T", enumerable: false}); 
//Object.getOwnPropertySymbols nos da un Array con claves de 
//símbolos enumerables y no enumerables propias (no del prototipo)
let keys = Object.getOwnPropertySymbols(arr);
console.log(keys); // [Symbol(s), Symbol(t)]
//Ahora iteramos con un bucle for clásico 
let cad = "";
for (let i=0; i<keys.length; i++){
    let key = keys[i].toString();
    cad += (cad?", ":"") + key + ': ' + arr[keys[i]];
}
console.log(cad); // "Symbol(s): S, Symbol(t): T"
    

El método Object.entries() nos devuelve un Array cuyos elementos son a su vez Array de dos posiciones, la primera contiene la clave y la segunda el valor. Luego podemos iterar por ese Array de dos dimensiones para extraer sus elementos. O bien con destructuring y un bucle for of como veremos en el apartado siguiente.

let arr = ["a", "b"];
let pares = Object.entries(arr);
console.log(pares); // [["0", "a"], ["1", "b"]]
let cad = "";
for (let [i, v] of pares){
    cad += (cad?", ":"") + i + ': ' + v;
}
console.log(cad); // "0: a, 1: b" NO FUNCIONA en Chrome 51 ni Firefox 46
    

El problema con el método Object.entries() es que aún no es soportado por los navegadores actuales Chrome 51 o Firefox 46 (previsto en ES8). Sin embargo ya si se soporta ese método en el prototipo de Array como veremos también en el siguiente apartado sobre iterables.

Los métodos estáticos de Object aplicados a un Array tampoco son adecuados para iterarlos. Eso lo tiene en cuenta ES6 y aplica los métodos anteriores de Object para tenerlos disponibles en el prototipo de Array, como veremos en el siguiente apartado.

Iterar por un Array con iterables. El bucle For-of.

Figura
Figura. Un Array tiene el método Symbol.iterator.

Un objeto es iterable si posee el método Symbol.iterator. Su ejecución devuelve un objeto iterador que sirve para iterar por el objeto. Nos puede servir para aplicar destructuring obteniéndose un Array. O para iterar en un bucle for of. Son iterables los built-in String, Array, Set, Map y los pertenecientes al grupo TypedArray (Arrays tipados).

El bucle for of es óptimo para iterar por Arrays, como el clásico for que nos permitía movernos en el rango [0..length-1] para iterar por todas las posiciones, recuperándose sólo las propiedades con claves numéricas enteras no negativas. Pero ahora ya no tenemos que preocuparnos de los rangos, pues el for of nos devolverá los valores sin necesidad de usar los índices. De hecho la mayor parte de las veces sólo estamos interesados en los valores de sus elementos, no en sus índices.

let arr = ["a", "b"];
arr["key"] = "xyz"; // Claves no númericas no son iterables
//Enumerables y no enumerables con claves numéricas son iterables
Object.defineProperty(arr, 2, {value: "c", enumerable: true});
Object.defineProperty(arr, 3, {value: "d", enumerable: false});
let cad = "";
for (let item of arr){
    cad += (cad?", ":"")  + item;
}
console.log(cad); // "a, b, c, d"
    

La ejecución del método arr.values() también puede servir como fuente iterable del bucle for of:

let arr = ["a", "b"];
console.log(arr.values()); // ArrayIterator{}
console.log([...arr.values()]); // ["a", "b"]
//Bucle For-of sobre arr.values()
let cad = "";
for (let item of arr.values()){
    cad += (cad?", ":"") + item;
}
console.log(cad); // "a, b"
    

De hecho la ejecución de ese método arr.values() es equivalente a arr[Symbol.iterator](), devolviendo ambos el mismo objeto iterador. En la Figura puede ver que ambas propiedades values y Symbol.iterator apuntan a la función values(). El método adjudicado a Symbol.iterator es llamado el método iterador predeterminado, usándose por ejemplo en un bucle for of cuando es aplicado directamente sobre un Array.

let arr = ["a", "b"];
let cad = "";
//Como para un Array el método predeterminado Symbol.iterator
//es values(), da lo mismo usar el Array directamente, 
//ejecutar arr.values() o arr[Symbol.iterator]()
for (let item of arr[Symbol.iterator]()){
    cad += (cad?", ":"") + item;
}
console.log(cad); // "a, b"
    

El objeto iterador tiene un método next() que va recuperando elementos del Array con cada ejecución. Cuando se completen el valor será undefined.

let arr = ["a", "b"];
let iterador = arr.values();
console.log(iterador.next()); // {value: "a", done: false}
console.log(iterador.next()); // {value: "b", done: false}
console.log(iterador.next()); // {value: undefined, done: true}
    

En el trasfondo de un for of está el uso de lo anterior. Podríamos construirnos un bucle que hiciera algo parecido con lo siguiente:

let arr = ["a", "b"];
let iterador = arr.values();
let item, cad = "";
while (item = iterador.next(), !item.done) {
    cad += (cad?", ":"") + item.value;
}
console.log(cad); // "a, b"
    

Con lo anterior sólo recuperábamos los valores del Array. Si aún estamos interesados en recuperar los índices podemos usar el método arr.keys(), que nos devuelve también un iterador.

let arr = ["a", "b"];
console.log(arr.keys()); // ArrayIterator{}
console.log([...arr.keys()]); // [0, 1]
//Bucle For-of sobre arr.keys()
let cad = ""; 
for (let key of arr.keys()) {
    cad += (cad?", ":"") + arr[key];
}
console.log(cad); // "a, b"
    

También podemos recuperar parejas índice y valor con arr.entries(). El método devuelve un iterador de Array. Igual que con arr.keys(), podemos usarlo en un for of ayudándonos del destructuring para asignar las parejas a las variables i y v:

let arr = ["a", "b"];
console.log(arr.entries()); // ArrayIterator{}
console.log([...arr.entries()]); // [["0", "a"], ["1", "b"]]
//Bucle For-of sobre arr.entries()
let cad = "";
for (let [i, v] of arr.entries()){
    cad += (cad?", ":"") + i + ': ' + v;
}
console.log(cad); // "0: a, 1: b"
    
Figura
Figura. El método entries() es el iterador predeterminado de un mapa.

Para un Array o un Set el método values() es el iterador por defecto. Mientras que para un Map es entries(). Como se observa en la Figura, el Map tiene un método values() como el Array, pero su [Symbol.iterator] apunta al método entries().

En un bucle for of ejecutado sobre un Map directamente, JavaScript usará ese método predeterminado entries() como se observa en el código siguiente:

let mapa = new Map([[0, "a"], [1, "b"]]);
let cad = "";
for (let [i, v] of mapa){
    cad += (cad?", ":"") + i + ': ' + v;
}
console.log(cad); // "0: a, 1: b"
    

Otros detalles de los bucles for of

En un bucle for (let item of arr) encontramos la fuente iterable (arr) y la variable destino (item). Ese destino puede ser cualquier otra cosa que acepte un valor de la fuente iterable, como este código que convierte un Array en un objeto:

let arr = ["a", "b"], obj = {}, i = 0;
for (obj[i++] of arr){}
console.log(obj); // Object {0: "a", 1: "b"}
    

En un bucle for clásico no podemos usar const puesto que en la segunda iteración nos lanzará un error de que no podemos reasignar una constante:

let arr = ["a", "b"],  cad = "";
for (const i=0; i<arr.length; i++){
    console.log(arr[i]);
}
// a
// TypeError: Assignment to constant variable.
    

En el bucle for of sin embargo cada iteración supone una nueva declaración de variable, por lo que se trata de una nueva declaración de constante.

let arr = ["a", "b"],  cad = "";
for (const item of arr){
    console.log(item);
}
// "a"
// "b"    
    

Esto podría ser útil para impedir reasignar por error la variable item dentro del bucle

let arr = ["a", "b"],  cad = "";
for (const item of arr){
    try {
        item = 1; 
    } catch(e){
        console.log(e.message); // Assignment to constant variable.
    }
} 
    

Tanto const como let producen una nueva declaración de la variable destino en cada iteración de un bucle for of. Pero no pasa con var, no volviéndose a redeclarar la variable. Esto tiene efectos relacionados con el closure si creamos y guardamos funciones dentro del bucle que referencie a la variable destino. En el siguiente ejemplo vamos guardando funciones flecha que devuelven los valores del Array. Luego vamos a ejecutar esa serie de funciones que deberán devolvernos los elementos del Array.

let arr = ["a", "b"], funs = [];
for (var item of arr){ // let item, const item
    funs.push(()=>item);
}
let cad = "";
for (let fun of funs){
    cad += (cad?", ":"")  + fun();    
}
// Con var obtenemos "b, b"
// Con let y const obtenemos "a, b"
console.log(cad);  
    

Con var todas nos devuelven el último valor que tomó la variable en el primer bucle. Pues todos los valores de item de cada una de las funciones están apuntando a la misma variable item declarada con var. Con const o let no sucede porque en cada iteración del primer bucle se crea una variable diferente.