Los vectores Array en ES6
Un Array es un vector en JavaScript
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]"
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
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
En definitiva, que reducir la longitud supone que se eliminen efectivamente los elementos del Array.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 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=0 | Con arr.splice(0) | ||
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écnica | para 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
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):
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
"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 | ||
---|---|---|---|---|
Cacheado | No cacheado | Diferencia | ||
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.
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"
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.