Introducción
El estamento for-in es uno de esos pocos incomprendidos en Javascript. Su papel principal es el de recorrer un objeto pasando por cada una de sus propiedades para actuar sobre ellas de alguna manera; sin embargo, dado que en Javascript los arrays son también objetos, podemos encontrarlo iterando por cada uno de los valores de estos últimos.
Se han escrito muchos artículos al respecto de este uso, sobre si es correcto o no, sobre el orden de enumeración obtenido, rendimiento, etc… Echemos un vistazo detenido a esta instrucción para ver cómo funciona con más detalle y corroborar o desmentir lo que sobre ella se dice.
Empecemos por lo básico
La especificación ECMAScript 262 define dos modos diferentes de declaración:
// Method 1: for ( var VariableDeclarationNoIn in Expression ) // Method 2: for ( LeftHandSideExpression in Expression ) |
Como vemos a simple vista, la única diferencia entre ambas fórmulas es el uso de var junto al primero de sus términos.
El método 1 es el más común y el que nos puede resultar más familiar. Declaramos una variable la cual recogerá a cada iteración el valor de los atributos que componen el objeto a recorrer. Destaco la palabra objeto porque con ella quiero englobar tanto al objeto considerado clásico (aquel de propiedades definidas mediante pares de nombre/valor), como a los array ya mencionados.
Un ejemplo sencillo sería el siguiente:
var myObj = { foo : 1, bar : 2, anotherProperty : 3 } for( var property in myObj ){ console.log( property ); } // foo // bar // anotherProperty |
Nótese que el valor devuelto se corresponde con el nombre de la propiedad; para acceder a su contenido, tendríamos que utilizar dicho nombre como índice del objeto:
var myObj = { foo : 1, bar : 2, anotherProperty : 3 } for( var property in myObj ){ console.log( myObj[property] ); } // 1 // 2 // 3 |
El segundo método, es más interesante. El término LeftHandSideExpresion hace referencia a cualquier tipo de expresión que pueda ser evaluada por el intérprete Javascript. Utilizando este patrón, asignamos en cada iteración el nombre de la propiedad a la expresión resultante del primer término.
La explicación puede parecer confusa, pero un ejemplo rápido nos muestra su funcionamiento:
var myObj = { foo : 1, bar : 2, anotherProperty : 3 }, myArr = [], i = 0; for( myArr[i++] in myObj ); console.log( myArr ); // ["foo", "bar", "anotherProperty"] |
De nuevo, observamos que obtenemos los nombres de las propiedades y no sus valores.
NOTA: Hay que tener cuidado con el punto y coma ; que finaliza el for-in: al aceptar este estamento un bloque con instrucciones a ejecutar en cada iteración, si omitimos el punto y coma, el intérprete tomará la siguiente línea que encuentre como instrucción a ejecutar provocando comportamientos no deseados. Por ejemplo:
var myObj = { foo : 1, bar : 2, anotherProperty : 3 }, myArr = [], i = 0; for( myArr[i++] in myObj ) // I forgot the ';'!! console.log( myArr ); // ["foo"] // ["foo", "bar"] // ["foo", "bar", "anotherProperty"] |
El console se ha tomado como instrucción de bloque y será ejecutado a cada iteración por las propiedades del objeto.
Como hemos mencionado, con este método solo se toman los nombres de las propiedades por lo que si queremos acceder también al valor, al no contar con ese nombre almacenado, debemos utilizar el propio array que estamos montando para preguntar al objeto:
var myObj = { foo : 1, bar : 2, anotherProperty : 3 }, myArr = [], myValues = [], i = 0; for( myArr[i++] in myObj ){ var lastItem = myArr.length - 1; myValues.push( myObj[ myArr[ lastItem ] ] ); } console.log( myValues ); // [ 1, 2, 3 ] |
No es un código muy elegante, pero parece ser la única forma de acceder al valor de una propiedad utilizando este tipo de estructura.
¿Cuánto de flexible se vuelve esto?
Podemos probar con formas más exóticas para ver qué ocurre.
Por ejemplo, la inicialización de varias variables (como haríamos por ejemplo en un for normal) produce en este caso un error inmediato:
var myObj = { foo : 1, bar : 2, anotherProperty : 3 }, myArr = [], myArr2 = [], i = 0; for( myArr[i], myArr2[i++] in myObj ); // SyntaxError: invalid for/in left-hand side |
Sin embargo, si que podemos alterar las expresiones mediante un doble in en nuestra estructura:
var myObj = { foo : 'Hello World' }, j = 'bar'; for( var i in myObj, j in myObj ); |
Esta compleja construcción en realidad no se corresponde con lo esperado: el segundo in actúa sobre la expresión evaluada en el primer término por lo que en realidad equivale a:
( [ var j = foo ] in [ bar in myObj ] ) |
Esta estructura, como nos comentan en @kuvos, tiene una consecuencia interesante en Javascript: eventualmente producirá un error, pero no en la fase de construcción sintáctica (es decir, no dará un error de sintaxis) sino en la de trazado del resultado. Esto hace a este estamento único dentro del lenguaje ya que es al parecer la única instrucción que requiere de un trazado hacia atrás una vez que el intérprete ha validado su sintaxis. Si esa expresión no resulta lógica, pese a ser correcta, producirá un error en tiempo de ejecución.
¿Sobre qué propiedades itera?
Pues según la especificación, esta instrucción itera por todas aquellas propiedades que poseen el valor interno [[Enumerable]] como verdadero. En la práctica, esto concierne a las propiedades definidas de facto en el objeto, a su prototipo, al prototipo del prototipo y así sucesivamente mientras nos remontamos en su cadena. Sin embargo, no se recogerían aquí las propiedades de los prototipos que hayan sido ocultadas por existir en dicha cadena prototípica un objeto anterior con una propiedad de igual nombre.
Para evitar todos estos elementos que en la mayoría de ocasiones solo suponen ruido en los resultados, Douglas Crockford nos recomienda el uso del método hasOwnProperty para filtrar todo aquello que provenga de herencia. Así tendríamos:
var arr = ['a','b','c'], indexesDefault = [], indexesClean = []; Array.prototype.each = function() {/*blah*/}; for (var index in arr) { indexesDefault.push( index ); if (arr.hasOwnProperty(index)) { indexesClean.push( index ); } } console.log( indexesDefault ); // ["0", "1", "2", "each"] console.log( indexesClean ); // ["0", "1", "2"] |
Como vemos, con hasOwnProperty podemos suprimir de la ecuación aquellas propiedades/métodos legados mediante herencia.
¿Y qué ocurre con los arrays?
Como se ha dicho al principio, esta instrucción permite iterar por las propiedades de un objeto y en Javascript, un array es un objeto más:
var myArr = [ 'foo', 'bar', 'hello']; typeof myArr // "object" |
Visto esto, podemos concluir pues que su uso es legítimo cuando se trata de recorrer los elementos que componen un array:
var myArr = [ 'foo', 'bar', 'hello']; for( var element in myArr ){ console.log( myArr[element] ); } // foo // bar // hello |
Nótese que en la iteración, element guarda el índice del array en lugar del valor; es por ello que debemos acceder al mismo utilizando la estructura myArr[element].
El resultado es el esperado. Sin embargo, debemos tener cuidado con una de las características de esta instrucción: el estándar no define el orden en que se recorrerá un objeto. De hecho, cuando recorremos un objeto tradicional, encontramos inconsistencias en un entorno multinavegador:
var obj = {3:'a', 2:'b', 'foo':'c', 1:'d'}, result = []; for (var prop in obj) { result.push(prop); } result.toString(); //Chrome -> "1, 2, 3, foo" //Other browsers -> "3, 2, foo, 1" |
En el caso de un array, parece respetarse el orden de los índices en todas las plataformas testadas. Sin embargo, al no estar estandarizado este comportamiento, no deberíamos confiar en que siempre se devuelvan los valores en el orden esperado. Si dicho orden no resulta crítico para el flujo de nuestra aplicación, podemos entonces utlizar este método de forma segura.
¿Y si hablamos de rendimiento?
Visto el punto anterior, puede resultar interesante analizar qué resulta más óptimo cara al rendimiento de un script: recorrer un array con un bucle for tradicional o mediante esta variante del for-in… Pues midamos!
Para realizar las métricas, utlizaremos el servicio online JSPerf en el que se han montado los siguientes scripts:
// Testing 'for' performance var myArr = [ 'En', 'un', 'lugar', 'de', 'la', 'Mancha', 'de', 'cuyo', 'nombre', 'no', 'quiero', 'acordarme' ], myArrClone = []; // Preparo un array de pruebas for( var x = 0, i = myArr.length; x < i; x++ ){ myArrClone.push( myArr[x] ); } |
Y, para la variante for-in:
// Testing 'for-in' performance var myArr = [ 'En', 'un', 'lugar', 'de', 'la', 'Mancha', 'de', 'cuyo', 'nombre', 'no', 'quiero', 'acordarme' ], myArrClone = []; for( var element in myArr ){ myArrClone.push( element ); } |
Los resultados son claros:
Cuando hablamos de recorrer un array, el método tradicional resulta hasta un 400% más rápido.
Conclusión
Tras revisar las métricas anteriores, queda de manifiesto que el uso del bucle for-in debería limitarse únicamente a recorrer un objeto tradicional (aquel que implementa pares de nombre/valor) y nunca un array. Esta afirmación responde a una cuestión de rendimiento patente en las estadísticas.
Una vez asumido el punto anterior, conviene saber que existen dos formas de construir este estamento: una básica en la que definimos una variable y a la que se asigna el nombre de la propiedad correspondiente en cada iteración y, una segunda en la que dicho nombre se asocia al resultado de evaluar una expresión. Con respecto a esta última estructura, descubrimos que podemos obtener errores inesperados debido a que el intérprete puede encontrarse con expresiones sin sentido (aún correctas sintácticamente) dentro de la lógica de la instrucción.
No podemos garantizar en ningún caso el orden en que se recorreran las propiedades de nuestro objeto por lo que, en caso de que dicho orden resulte crítico para nuestra aplicación, debemos buscar alguna forma alternativa de almacenamiento como, por ejemplo, un array tradicional.
Para saber más
Angus Croll, Exploring JavaScript for-in loops
Peter van der Zee, for-in grammar
ECMA262, The for-in Statement
Me has dado una idea, se podría probar lo mismo haciendo uso del pop y el shift de array con un while…
Magnífico artículo Carlos.
Creo necesario hacer ciertas consideraciones sobre el ejemplo del doble in que has construido basándote en el artículo de qfox. El ejemplo en cuestión funciona perfectamente y es la equivalencia que expone qfox la que no es correcta. La construcción del ejemplo es gramaticalmente válida y además no fallará eventualmente: no tiene nada de especial en el lenguaje.
Para mostrar lo que esta ocurriendo vamos a dotar al bucle de un cuerpo que nos vaya mostrando los sucesivos valores que la variable va tomando:
Si lo ejecutamos observaremos que no hay nada observable: no se imprime nada. Investiguemos un poco, sin tocar nada esencial del código del ejemplo, vamos a añadir un par de «inocentes» líneas:
¡Ahora sí obtenemos salida! Cuando lo ejecutamos obtenemos un consejo que afortunadamente podemos empezar a seguir en las implementaciones ES5. ¿Qué es lo que está pasando?
El parser interpretará la línea del for-in de la siguiente manera:
donde
Expression
es una expressión construida con el operador comay ya sabemos que este operador tendrá por valor lo que nos proporcione el último elemento de la lista
Como
j
vale'bar'
ymyObj
no tiene ninguna propiedad con dicho nombre, el valor deExpression
esfalse
. Es decir, la línea que estamos estudiando equivale aAhora el for-in convierte el valor de
Expression
en objeto. En consecuencia lo anterior equivale aPor tanto ahora se mostrarán los nombres de las propiedades enumerables de ese objeto Boolean. En principio no tenía, pero con el par de líneas añadidas, el prototipo de
Boolean
ya tiene algo que enumerar, y obtenemos un mensaje.Resumiendo, no hay ningún misterio, y un par de paréntesis a veces son una gran ayuda
Por cierto, lo que expone qfox no es correcto y creo que es el origen de la confusión:
INTERESANTE ARTICULO. UNA PREGUNTA EL FOR IN FUNCIONA EN TODOS LOS NAVEGADORES?
Si; es una instrucción nativa que pertenece a la especificación ECMAScript262. Ésto debería garantizar su presencia en todos los navegadores.
Saludos!
Muy buena entrada, me sirvió de mucho 🙂
¡Gracias!