¿Por qué los objetos no son iterables en JavaScript?
arrays iterator (6)
¿Por qué los objetos no son iterables por defecto?
Veo preguntas todo el tiempo relacionadas con la iteración de objetos, la solución común es iterar sobre las propiedades de un objeto y acceder a los valores dentro de un objeto de esa manera. Esto parece tan común que me hace preguntarme por qué los objetos en sí no son iterables.
Declaraciones como el ES6
for...of
sería bueno usar para objetos por defecto.
Debido a que estas características solo están disponibles para "objetos iterables" especiales que no incluyen objetos
{}
, tenemos que pasar por aros para que esto funcione para los objetos para los que queremos usarlo.
La instrucción for ... of crea un bucle que itera sobre objetos iterables (incluidos Array, Map, Set, argumentos, etc.) ...
Por ejemplo, usando una función de generador ES6:
var example = {a: {e: ''one'', f: ''two''}, b: {g: ''three''}, c: {h: ''four'', i: ''five''}};
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
for (let [key, value] of entries(example)) {
console.log(key);
console.log(value);
for (let [key, value] of entries(value)) {
console.log(key);
console.log(value);
}
}
Lo anterior registra correctamente los datos en el orden que espero cuando ejecuto el código en Firefox (que admite ES6 ):
Por defecto, los objetos
{}
no son iterables, pero ¿por qué?
¿Las desventajas superarían los beneficios potenciales de que los objetos sean iterables?
¿Cuáles son los problemas asociados con esto?
Además, dado que los objetos
{}
son diferentes de las colecciones "tipo matriz" y los "objetos iterables" como
NodeList
,
HtmlCollection
y
arguments
, no se pueden convertir en matrices.
Por ejemplo:
var argumentsArray = Array.prototype.slice.call(arguments);
o ser utilizado con los métodos de matriz:
Array.prototype.forEach.call(nodeList, function (element) {})
.
Además de las preguntas que tengo arriba, me encantaría ver un ejemplo de trabajo sobre cómo convertir
{}
objetos en iterables, especialmente de aquellos que han mencionado el
[Symbol.iterator]
.
Esto debería permitir que estos nuevos
{}
"objetos iterables" utilicen sentencias como
for...of
.
Además, me pregunto si hacer que los objetos sean iterables les permite convertirse en matrices.
Intenté el siguiente código, pero obtengo un
TypeError: can''t convert undefined to object
.
var example = {a: {e: ''one'', f: ''two''}, b: {g: ''three''}, c: {h: ''four'', i: ''five''}};
// I want to be able to use "for...of" for the "example" object.
// I also want to be able to convert the "example" object into an Array.
example[Symbol.iterator] = function* (obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
};
for (let [key, value] of example) { console.log(value); } // error
console.log([...example]); // error
Este es el último enfoque (que funciona en Chrome Chrome)
var files = {
''/root'': {type: ''directory''},
''/root/example.txt'': {type: ''file''}
};
for (let [key, {type}] of Object.entries(files)) {
console.log(type);
}
Sí, las
entries
ahora son un método que forma parte de Object :)
editar
Después de analizarlo más, parece que podrías hacer lo siguiente
Object.prototype[Symbol.iterator] = function * () {
for (const [key, value] of Object.entries(this)) {
yield {key, value}; // or [key, value]
}
};
así que ahora puedes hacer esto
for (const {key, value:{type}} of files) {
console.log(key, type);
}
edit2
Volviendo a su ejemplo original, si desea utilizar el método prototipo anterior, le gustaría que esto
for (const {key, value:item1} of example) {
console.log(key);
console.log(item1);
for (const {key, value:item2} of item1) {
console.log(key);
console.log(item2);
}
}
Puede hacer que todos los objetos sean iterables globalmente:
Object.defineProperty(Object.prototype, Symbol.iterator, {
enumerable: false,
value: function * (){
for(let key in this){
if(this.hasOwnProperty(key)){
yield [key, this[key]];
}
}
}
});
Supongo que la pregunta debería ser "¿por qué no hay iteración de objetos incorporada ?
Agregar iterabilidad a los propios objetos podría tener consecuencias no deseadas, y no, no hay forma de garantizar el orden, pero escribir un iterador es tan simple como
function* iterate_object(o) {
var keys = Object.keys(o);
for (var i=0; i<keys.length; i++) {
yield [keys[i], o[keys[i]]];
}
}
Entonces
for (var [key, val] of iterate_object({a: 1, b: 2})) {
console.log(key, val);
}
a 1
b 2
Técnicamente, esta no es una respuesta a la pregunta ¿por qué? pero he adaptado la respuesta de Jack Slocum anterior a la luz de los comentarios de BT a algo que se puede usar para hacer que un objeto sea iterable.
var iterableProperties={
enumerable: false,
value: function * () {
for(let key in this) if(this.hasOwnProperty(key)) yield this[key];
}
};
var fruit={
''a'': ''apple'',
''b'': ''banana'',
''c'': ''cherry''
};
Object.defineProperty(fruit,Symbol.iterator,iterableProperties);
for(let v of fruit) console.log(v);
No es tan conveniente como debería haber sido, pero es viable, especialmente si tiene varios objetos:
var instruments={
''a'': ''accordion'',
''b'': ''banjo'',
''c'': ''cor anglais''
};
Object.defineProperty(instruments,Symbol.iterator,iterableProperties);
for(let v of instruments) console.log(v);
Y, dado que todos tienen derecho a una opinión, tampoco puedo ver por qué los objetos no son iterables.
Si puede rellenarlos como anteriormente, o usarlos
for … in
ese momento, no puedo ver un argumento simple.
Una posible sugerencia es que lo que es iterable es un tipo de objeto, por lo que es posible que iterable se haya limitado a un subconjunto de objetos en caso de que otros objetos exploten en el intento.
Voy a intentar esto. Tenga en cuenta que no estoy afiliado a ECMA y no tengo visibilidad en su proceso de toma de decisiones, por lo que no puedo decir definitivamente por qué han hecho o no han hecho nada. Sin embargo, expondré mis suposiciones y haré mi mejor intento.
1. ¿Por qué agregar un
for...of
construcción en primer lugar?
JavaScript ya incluye una construcción
for...in
que se puede usar para iterar las propiedades de un objeto.
Sin embargo, no es
realmente un ciclo forEach
, ya que enumera todas las propiedades de un objeto y tiende a funcionar de manera predecible en casos simples.
Se desglosa en casos más complejos (incluso con matrices, donde su uso tiende a ser
desalentado o completamente ofuscado
por las salvaguardas necesarias para usar
for...in
con una matriz
correctamente
).
Puede
hasOwnProperty
utilizando
hasOwnProperty
(entre otras cosas), pero eso es un poco torpe e poco elegante.
Por lo tanto, mi suposición es que el
for...of
construct se está agregando para abordar las deficiencias asociadas con el
for...in
construct, y proporcionar una mayor utilidad y flexibilidad al iterar las cosas.
Las personas tienden a tratar
for...in
como un bucle
forEach
que generalmente se puede aplicar a cualquier colección y producir resultados sanos en cualquier contexto posible, pero eso no es lo que sucede.
El
for...of
loop soluciona eso.
También supongo que es importante que el código ES5 existente se ejecute en ES6 y produzca el mismo resultado que en ES5, por lo que no se pueden realizar cambios importantes, por ejemplo, en el comportamiento de
for...in
construct.
2. ¿Cómo funciona
for...of
trabajo?
La
documentación de referencia
es útil para esta parte.
Específicamente, un objeto se considera
iterable
si define la propiedad
Symbol.iterator
.
La definición de propiedad debe ser una función que devuelva los elementos de la colección, uno, por, uno, y establezca un indicador que indique si hay más elementos para recuperar.
Se proporcionan implementaciones predefinidas para
algunos tipos de objetos
, y es relativamente claro que usar
for...of
simplemente delega a la función de iterador.
Este enfoque es útil, ya que hace que sea muy sencillo proporcionar sus propios iteradores.
Podría decir que el enfoque podría haber presentado problemas prácticos debido a su dependencia de definir una propiedad donde anteriormente no había ninguna, excepto por lo que puedo decir que ese no es el caso, ya que la nueva propiedad se ignora esencialmente a menos que la busque deliberadamente (es decir no se presentará en
for...in
loops como una clave, etc.).
Entonces ese no es el caso.
Dejando a un lado los problemas prácticos, puede haberse considerado conceptualmente controvertido comenzar todos los objetos con una nueva propiedad predefinida, o decir implícitamente que "cada objeto es una colección".
3. ¿Por qué los objetos no son
iterable
usando
for...of
por defecto?
Mi conjetura es que esta es una combinación de:
-
Hacer que todos los objetos sean
iterable
por defecto puede haberse considerado inaceptable porque agrega una propiedad donde anteriormente no había ninguno, o porque un objeto no es (necesariamente) una colección. Como señala Felix, "¿qué significa iterar sobre una función o un objeto de expresión regular"? -
Los objetos simples ya se pueden iterar usando
for...in
, y no está claro qué podría haber hecho una implementación de iterador integrada diferente / mejor que la existentefor...in
comportamiento. Entonces, incluso si el n. ° 1 es incorrecto y agregar la propiedad fue aceptable, puede que no se haya visto como útil . -
Los usuarios que desean que sus objetos sean
iterable
pueden hacerlo fácilmente definiendo la propiedadSymbol.iterator
. -
La especificación ES6 también proporciona un tipo de
Map
, que
es
iterable
por defecto y tiene algunas otras pequeñas ventajas sobre el uso de un objeto plano como unMap
.
Incluso hay un ejemplo proporcionado para el n. ° 3 en la documentación de referencia:
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
for (var value of myIterable) {
console.log(value);
}
Dado que los objetos se pueden hacer fácilmente
iterable
, que ya se pueden iterar usando
for...in
, y que es probable que no haya un acuerdo claro sobre lo que debe hacer un iterador de objetos predeterminado (si lo que hace debe ser de alguna manera diferente de lo que
for...in
does), parece bastante razonable que los objetos no se hayan hecho
iterable
por defecto.
Tenga en cuenta que su código de ejemplo puede reescribirse usando
for...in
:
for (let levelOneKey in object) {
console.log(levelOneKey); // "example"
console.log(object[levelOneKey]); // {"random":"nest","another":"thing"}
var levelTwoObj = object[levelOneKey];
for (let levelTwoKey in levelTwoObj ) {
console.log(levelTwoKey); // "random"
console.log(levelTwoObj[levelTwoKey]); // "nest"
}
}
... o también puede hacer que su objeto sea
iterable
de la manera que desee haciendo algo como lo siguiente (o puede hacer que
todos los
objetos sean
iterable
asignando a
Object.prototype[Symbol.iterator]
):
obj = {
a: ''1'',
b: { something: ''else'' },
c: 4,
d: { nested: { nestedAgain: true }}
};
obj[Symbol.iterator] = function() {
var keys = [];
var ref = this;
for (var key in this) {
//note: can do hasOwnProperty() here, etc.
keys.push(key);
}
return {
next: function() {
if (this._keys && this._obj && this._index < this._keys.length) {
var key = this._keys[this._index];
this._index++;
return { key: key, value: this._obj[key], done: false };
} else {
return { done: true };
}
},
_index: 0,
_keys: keys,
_obj: ref
};
};
Puedes jugar con eso aquí (en Chrome, en arrendamiento): http://jsfiddle.net/rncr3ppz/5/
Editar
Y en respuesta a su pregunta actualizada, sí, es posible convertir un
iterable
en una matriz, utilizando el
operador de propagación
en ES6.
Sin embargo, esto todavía no parece funcionar en Chrome, o al menos no puedo hacerlo funcionar en mi jsFiddle. En teoría debería ser tan simple como:
var array = [...myIterable];
Object
no implementan los protocolos de iteración en Javascript por muy buenas razones.
Hay dos niveles en los que las propiedades de los objetos se pueden iterar en JavaScript:
- el nivel del programa
- el nivel de datos
Programa de iteración de nivel
Cuando itera sobre un objeto en el nivel del programa, examina una parte de la estructura de su programa. Es una operación reflexiva. Vamos a ilustrar esta declaración con un tipo de matriz, que generalmente se repite en el nivel de datos:
const xs = [1,2,3];
xs.f = function f() {};
for (let i in xs) console.log(xs[i]); // logs `f` as well
Acabamos de examinar el nivel del programa de
xs
.
Dado que las matrices almacenan secuencias de datos, regularmente solo estamos interesados en el nivel de datos.
for..in
evidentemente no tiene sentido en relación con matrices y otras estructuras "orientadas a datos" en la mayoría de los casos.
Esa es la razón por la cual ES2015 ha introducido para
for..of
y el protocolo iterable.
Iteración del nivel de datos
¿Significa eso que simplemente podemos distinguir los datos del nivel del programa distinguiendo las funciones de los tipos primitivos? No, porque las funciones también pueden ser datos en Javascript:
-
Array.prototype.sort
por ejemplo, espera que una función realice un cierto algoritmo de ordenación -
Thunks como
() => 1 + 2
son solo envoltorios funcionales para valores vagamente evaluados
Además, los valores primitivos también pueden representar el nivel del programa:
-
[].length
por ejemplo, es unNumber
pero representa la longitud de una matriz y, por lo tanto, pertenece al dominio del programa
Eso significa que no podemos distinguir el programa y el nivel de datos simplemente comprobando los tipos.
Es importante comprender que la implementación de los protocolos de iteración para objetos Javascript antiguos simples dependería del nivel de datos. Pero como acabamos de ver, no es posible una distinción confiable entre los datos y la iteración a nivel de programa.
Con
Array
s esta distinción es trivial: cada elemento con una clave de tipo entero es un elemento de datos.
Object
tienen una característica equivalente: el descriptor
enumerable
.
Pero, ¿es realmente recomendable confiar en esto?
¡Creo que no lo es!
El significado del descriptor
enumerable
es demasiado borroso.
Conclusión
No hay una manera significativa de implementar los protocolos de iteración para los objetos, porque no todos los objetos son una colección.
Si las propiedades del objeto eran iterables de manera predeterminada, el nivel de datos y el programa estaban mezclados.
Dado que cada tipo compuesto en Javascript se basa en objetos simples, esto también se aplicaría a
Array
y
Map
.
for..in
,
for..in
,
Object.keys
, etc. se pueden usar tanto para la reflexión como para la iteración de datos, una distinción clara no es posible.
Si no tienes cuidado, terminas rápidamente con meta programación y dependencias extrañas.
El tipo de datos abstractos de
Map
termina efectivamente la combinación de programa y nivel de datos.
Creo que
Map
es el logro más significativo en ES2015, incluso si
Promise
s es mucho más emocionante.