Pourquoi les objets ne sont-ils pas itérables en JavaScript?

87

Pourquoi les objets ne sont-ils pas itérables par défaut?

Je vois tout le temps des questions liées à l'itération d'objets, la solution courante étant de parcourir les propriétés d'un objet et d'accéder aux valeurs d'un objet de cette façon. Cela semble si courant que je me demande pourquoi les objets eux-mêmes ne sont pas itérables.

Des déclarations comme l'ES6 for...ofseraient bien utilisées pour les objets par défaut. Parce que ces fonctionnalités ne sont disponibles que pour les "objets itérables" spéciaux qui n'incluent pas d' {}objets, nous devons passer par des cercles pour que cela fonctionne pour les objets pour lesquels nous voulons l'utiliser.

L'instruction for ... of crée une boucle itérant sur des objets itérables (y compris Array, Map, Set, arguments object et ainsi de suite) ...

Par exemple en utilisant une fonction de générateur 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);
  }
}

Ce qui précède enregistre correctement les données dans l'ordre dans lequel je m'attends lorsque j'exécute le code dans Firefox (qui prend en charge ES6 ):

sortie de hacky pour ... de

Par défaut, les {}objets ne sont pas itérables, mais pourquoi? Les inconvénients l'emporteraient-ils sur les avantages potentiels des objets itérables? Quels sont les problèmes associés à cela?

De plus, parce que les {}objets sont différents des « Array comme » collections « et des objets itérables » tels que NodeList, HtmlCollectionet arguments, ils ne peuvent pas être convertis en tableaux.

Par exemple:

var argumentsArray = Array.prototype.slice.call(arguments);

ou être utilisé avec les méthodes Array:

Array.prototype.forEach.call(nodeList, function (element) {}).

Outre les questions que j'ai ci-dessus, j'aimerais voir un exemple de travail sur la façon de transformer des {}objets en itérables, en particulier de ceux qui ont mentionné le [Symbol.iterator]. Cela devrait permettre à ces nouveaux {}"objets itérables" d'utiliser des instructions comme for...of. Aussi, je me demande si rendre les objets itérables permet de les convertir en tableaux.

J'ai essayé le code ci-dessous, mais j'obtiens 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
boombox
la source
1
Tout ce qui a une Symbol.iteratorpropriété est un itérable. Il vous suffit donc de mettre en œuvre cette propriété. Une explication possible de la raison pour laquelle les objets ne sont pas itérables pourrait être que cela impliquerait que tout était itérable, puisque tout est un objet (sauf les primitifs bien sûr). Cependant, que signifie itérer sur une fonction ou un objet d'expression régulière?
Felix Kling
7
Quelle est votre vraie question ici? Pourquoi l'ECMA a-t-elle pris les décisions qu'elle a prises?
Steve Bennett
3
Puisque les objets n'ont AUCUN ordre garanti de leurs propriétés, je me demande si cela rompt avec la définition d'un itérable dont vous vous attendriez à avoir un ordre prévisible?
jfriend00
2
Pour obtenir une réponse faisant autorité pour "pourquoi", vous devriez demander à esdiscuss.org
Felix Kling
1
@FelixKling - est-ce que ce message concerne ES6? Vous devriez probablement le modifier pour indiquer de quelle version vous parlez car la "version à venir d'ECMAScript" ne fonctionne pas très bien avec le temps.
jfriend00

Réponses:

42

Je vais essayer. Notez que je ne suis pas affilié à l'ECMA et que je n'ai aucune visibilité sur leur processus de prise de décision, je ne peux donc pas dire avec certitude pourquoi ils ont ou n'ont rien fait. Cependant, je vais exposer mes hypothèses et faire de mon mieux.

1. Pourquoi ajouter une for...ofconstruction en premier lieu?

JavaScript inclut déjà une for...inconstruction qui peut être utilisée pour itérer les propriétés d'un objet. Cependant, ce n'est pas vraiment une boucle forEach , car elle énumère toutes les propriétés d'un objet et a tendance à ne fonctionner de manière prévisible que dans des cas simples.

Il se décompose dans des cas plus complexes (y compris avec des baies, où son utilisation a tendance à être soit découragée, soit complètement obscurcie par les garanties nécessaires pour une utilisation for...inavec une baie correcte ). Vous pouvez contourner cela en utilisant hasOwnProperty(entre autres choses), mais c'est un peu maladroit et inélégant.

Donc, mon hypothèse est que le for...ofconcept est ajouté pour combler les lacunes associées au for...inconcept et pour offrir une plus grande utilité et flexibilité lors de l'itération des choses. Les gens ont tendance à traiter for...incomme unforEach boucle qui peut généralement être appliquée à n'importe quelle collection et produire des résultats sains dans n'importe quel contexte possible, mais ce n'est pas ce qui se passe. La for...ofboucle corrige cela.

Je suppose également qu'il est important pour le code ES5 existant de fonctionner sous ES6 et de produire le même résultat que sous ES5, de sorte que des modifications radicales ne peuvent pas être apportées, par exemple, au comportement de la for...inconstruction.

2. Comment ça for...ofmarche?

La documentation de référence est utile pour cette partie. Plus précisément, un objet est considéré iterables'il définit la Symbol.iteratorpropriété.

La définition de propriété doit être une fonction qui renvoie les éléments de la collection, un, par, un, et définit un indicateur indiquant s'il y a ou non plus d'éléments à récupérer. Des implémentations prédéfinies sont fournies pour certains types d'objets , et il est relativement clair que l'utilisation de for...ofdélégués simplement à la fonction d'itérateur.

Cette approche est utile, car elle permet de fournir très simplement vos propres itérateurs. Je pourrais dire que l'approche aurait pu présenter des problèmes pratiques en raison de sa dépendance à la définition d'une propriété là où il n'y en avait pas auparavant, sauf de ce que je peux dire, ce n'est pas le cas car la nouvelle propriété est essentiellement ignorée à moins que vous ne la cherchiez délibérément (c.-à-d. il ne se présentera pas dans les for...inboucles comme clé, etc.) Ce n'est donc pas le cas.

Mis à part les problèmes pratiques, il peut avoir été considéré comme controversé sur le plan conceptuel de commencer tous les objets avec une nouvelle propriété prédéfinie, ou de dire implicitement que «chaque objet est une collection».

3. Pourquoi les objets ne sont-ils pas iterableutilisés for...ofpar défaut?

Je suppose que c'est une combinaison de:

  1. La création de tous les objets iterablepar défaut peut avoir été considérée comme inacceptable car elle ajoute une propriété là où il n'y en avait pas auparavant, ou parce qu'un objet n'est pas (nécessairement) une collection. Comme le note Felix, "que signifie itérer sur une fonction ou un objet d'expression régulière"?
  2. Les objets simples peuvent déjà être itérés à l'aide for...in, et on ne sait pas ce qu'une implémentation d'itérateur intégré aurait pu faire différemment / mieux que le for...incomportement existant . Donc, même si # 1 est faux et que l'ajout de la propriété était acceptable, cela n'a peut-être pas été considéré comme utile .
  3. Les utilisateurs qui souhaitent créer leurs objets iterablepeuvent le faire facilement, en définissant la Symbol.iteratorpropriété.
  4. La spécification ES6 fournit également un type de carte , qui est iterable par défaut et présente d'autres petits avantages par rapport à l'utilisation d'un objet simple commeMap .

Il y a même un exemple fourni pour # 3 dans la documentation de référence:

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};

for (var value of myIterable) {
    console.log(value);
}

Étant donné que les objets peuvent être facilement créés iterable, qu'ils peuvent déjà être itérés en utilisant for...in, et qu'il n'y a probablement pas d'accord clair sur ce qu'un itérateur d'objet par défaut devrait faire (si ce qu'il fait est censé être quelque peu différent de ce qu'il for...infait), cela semble raisonnable assez pour que les objets ne soient pas créés iterablepar défaut.

Notez que votre exemple de code peut être réécrit en utilisant 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"
    }
}

... ou vous pouvez également créer votre objet iterablecomme vous le souhaitez en faisant quelque chose comme ce qui suit (ou vous pouvez créer tous les objets iterableen attribuant à la Object.prototype[Symbol.iterator]place):

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
    };
};

Vous pouvez jouer avec ça ici (dans Chrome, au bail): http://jsfiddle.net/rncr3ppz/5/

Éditer

Et en réponse à votre question mise à jour, oui, il est possible de convertir un iterableen tableau, en utilisant l' opérateur de propagation dans ES6.

Cependant, cela ne semble pas encore fonctionner dans Chrome, ou du moins je ne peux pas le faire fonctionner dans mon jsFiddle. En théorie, cela devrait être aussi simple que:

var array = [...myIterable];
aroth
la source
Pourquoi ne pas simplement faire obj[Symbol.iterator] = obj[Symbol.enumerate]dans votre dernier exemple?
Bergi
@Bergi - Parce que je n'ai pas vu cela dans la documentation (et que je ne vois pas cette propriété décrite ici ). Bien qu'un argument en faveur de la définition explicite de l'itérateur soit que cela facilite l'application d'un ordre d'itération spécifique, si cela est nécessaire. Si l'ordre d'itération n'est pas important (ou si l'ordre par défaut est correct) et que le raccourci d'une ligne fonctionne, il y a peu de raisons de ne pas adopter l'approche plus concise, cependant.
aroth
Oups, ce [[enumerate]]n'est pas un symbole bien connu (@@ enumerate) mais une méthode interne. Je devrais êtreobj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
Bergi
À quoi servent toutes ces suppositions, alors que le processus réel de discussion est bien documenté? Il est très étrange que vous disiez "Donc, par conséquent, mon hypothèse est que le for ... of construit est ajouté pour corriger les lacunes associées au for ... in construct." Non. Il a été ajouté pour prendre en charge une manière générale d'itérer sur n'importe quoi, et fait partie d'un large éventail de nouvelles fonctionnalités, y compris les itérables eux-mêmes, les générateurs, les cartes et les ensembles. Il ne s'agit guère d'un remplacement ou d'une mise à niveau vers for...in, qui a un objectif différent: parcourir les propriétés d'un objet.
2
Bon point en soulignant à nouveau que tous les objets ne sont pas une collection. Les objets sont utilisés comme tels depuis longtemps, car c'était très pratique, mais finalement, ce ne sont pas vraiment des collections. C'est ce que nous avons Mappour le moment.
Felix Kling
9

Objects n'implémentent pas les protocoles d'itération en Javascript pour de très bonnes raisons. Il existe deux niveaux auxquels les propriétés des objets peuvent être itérées dans JavaScript:

  • le niveau du programme
  • le niveau de données

Itération au niveau du programme

Lorsque vous parcourez un objet au niveau du programme, vous examinez une partie de la structure de votre programme. C'est une opération réfléchissante. Illustrons cette déclaration avec un type de tableau, qui est généralement itéré au niveau des données:

const xs = [1,2,3];
xs.f = function f() {};

for (let i in xs) console.log(xs[i]); // logs `f` as well

Nous venons d'examiner le niveau de programme de xs. Puisque les tableaux stockent des séquences de données, nous nous intéressons régulièrement uniquement au niveau des données. for..inn'a évidemment aucun sens en relation avec les tableaux et autres structures «orientées données» dans la plupart des cas. C'est la raison pour laquelle ES2015 a introduit for..ofet le protocole itérable.

Itération du niveau de données

Cela signifie-t-il que nous pouvons simplement distinguer les données du niveau programme en distinguant les fonctions des types primitifs? Non, car les fonctions peuvent aussi être des données en Javascript:

  • Array.prototype.sort par exemple s'attend à ce qu'une fonction exécute un certain algorithme de tri
  • Les thunks comme ne () => 1 + 2sont que des wrappers fonctionnels pour des valeurs évaluées paresseusement

Outre les valeurs primitives peuvent également représenter le niveau du programme:

  • [].lengthpar exemple est a Numbermais représente la longueur d'un tableau et appartient donc au domaine du programme

Cela signifie que nous ne pouvons pas distinguer le programme et le niveau de données en vérifiant simplement les types.


Il est important de comprendre que la mise en œuvre des protocoles d'itération pour les anciens objets Javascript simples reposerait sur le niveau de données. Mais comme nous venons de le voir, une distinction fiable entre les données et l'itération au niveau du programme n'est pas possible.

Avec Arrays, cette distinction est triviale: chaque élément avec une clé de type entier est un élément de données. Objects ont une fonctionnalité équivalente: le enumerabledescripteur. Mais est-il vraiment conseillé de s'y fier? Je crois que non! La signification du enumerabledescripteur est trop floue.

Conclusion

Il n'existe aucun moyen significatif d'implémenter les protocoles d'itération pour les objets, car tous les objets ne sont pas une collection.

Si les propriétés de l'objet étaient itérables par défaut, le programme et le niveau de données étaient mélangés. Étant donné que chaque type composite en Javascript est basé sur des objets simples , cela s'appliquerait pour Arrayet Mapaussi bien.

for..in, Object.keys, Reflect.ownKeysEtc. peuvent être utilisés à la fois itération de réflexion et des données, une distinction claire est régulièrement pas possible. Si vous ne faites pas attention, vous vous retrouvez rapidement avec une métaprogrammation et des dépendances étranges. Le Maptype de données abstrait met effectivement fin à la fusion du programme et du niveau de données. Je crois que Mapc'est la réalisation la plus significative de l'ES2015, même si les Promises sont beaucoup plus excitantes.


la source
3
+1, je pense "Il n'y a pas de moyen significatif d'implémenter les protocoles d'itération pour les objets, car tous les objets ne sont pas une collection." résume.
Charlie Schliesser
1
Je ne pense pas que ce soit un bon argument. Si votre objet n'est pas une collection, pourquoi essayez-vous de le parcourir en boucle? Peu importe que tous les objets ne soient pas une collection, car vous n'essaierez pas de parcourir ceux qui ne le sont pas.
BT
En fait, chaque objet est une collection, et ce n'est pas au langage de décider si la collection est cohérente ou non. Les tableaux et les cartes peuvent également collecter des valeurs non liées. Le fait est que vous pouvez parcourir les clés de n'importe quel objet, quelle que soit leur utilisation, vous êtes donc à un pas de l'itération sur leurs valeurs. Si vous parliez d'un langage qui tape statiquement des valeurs de tableau (ou de toute autre collection), vous pourriez parler de ces restrictions, mais pas de JavaScript.
Manngo
Cet argument selon lequel chaque objet n'est pas une collection n'a pas de sens. Vous supposez qu'un itérateur n'a qu'un seul but (itérer une collection). L'itérateur par défaut sur un objet serait un itérateur des propriétés de l'objet, peu importe ce que ces propriétés représentent (qu'il s'agisse d'une collection ou autre). Comme l'a dit Manngo, si votre objet ne représente pas une collection, alors c'est au programmeur de ne pas le traiter comme une collection. Peut-être qu'ils veulent simplement itérer les propriétés de l'objet pour une sortie de débogage? Il y a beaucoup d'autres raisons en plus d'une collection.
jfriend00
8

Je suppose que la question devrait être «pourquoi n'y a-t-il pas d' itération d'objet intégrée ?

L'ajout d'itérabilité aux objets eux-mêmes pourrait éventuellement avoir des conséquences inattendues, et non, il n'y a aucun moyen de garantir l'ordre, mais écrire un itérateur est aussi simple que

function* iterate_object(o) {
    var keys = Object.keys(o);
    for (var i=0; i<keys.length; i++) {
        yield [keys[i], o[keys[i]]];
    }
}

ensuite

for (var [key, val] of iterate_object({a: 1, b: 2})) {
    console.log(key, val);
}

a 1
b 2

la source
1
merci torazaburo. j'ai révisé ma question. J'aimerais voir un exemple utilisant [Symbol.iterator]ainsi que si vous pouviez développer ces conséquences involontaires.
boombox
4

Vous pouvez facilement rendre tous les objets itérables globalement:

Object.defineProperty(Object.prototype, Symbol.iterator, {
    enumerable: false,
    value: function * (){
        for(let key in this){
            if(this.hasOwnProperty(key)){
                yield [key, this[key]];
            }
        }
    }
});
Jack Slocum
la source
3
N'ajoutez pas globalement de méthodes aux objets natifs. C'est une idée terrible qui va vous mordre, vous et quiconque utilise votre code, dans le cul.
BT
2

Il s'agit de la dernière approche (qui fonctionne dans Chrome Canary)

var files = {
    '/root': {type: 'directory'},
    '/root/example.txt': {type: 'file'}
};

for (let [key, {type}] of Object.entries(files)) {
    console.log(type);
}

Yes entriesest maintenant une méthode qui fait partie d'Object :)

Éditer

Après avoir examiné plus en détail, il semble que vous pourriez faire ce qui suit

Object.prototype[Symbol.iterator] = function * () {
    for (const [key, value] of Object.entries(this)) {
        yield {key, value}; // or [key, value]
    }
};

donc tu peux maintenant faire ça

for (const {key, value:{type}} of files) {
    console.log(key, type);
}

edit2

Revenez à votre exemple d'origine, si vous vouliez utiliser la méthode de prototype ci-dessus, cela devrait

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);
    }
}
Chad Scira
la source
2

J'ai également été dérangé par cette question.

Puis j'ai eu une idée d'utilisation Object.entries({...}), ça renvoie un Arrayqui est unIterable .

En outre, le Dr Axel Rauschmayer a publié une excellente réponse à ce sujet. Voir Pourquoi les objets simples ne sont PAS itérables

Romko
la source
0

Techniquement, ce n'est pas une réponse à la question pourquoi? mais j'ai adapté la réponse de Jack Slocum ci-dessus à la lumière des commentaires de BT à quelque chose qui peut être utilisé pour rendre un objet itérable.

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

Pas aussi pratique qu'il aurait dû l'être, mais c'est réalisable, surtout si vous avez plusieurs objets:

var instruments={
    'a': 'accordion',
    'b': 'banjo',
    'c': 'cor anglais'
};
Object.defineProperty(instruments,Symbol.iterator,iterableProperties);
for(let v of instruments) console.log(v);

Et, parce que chacun a droit à une opinion, je ne vois pas pourquoi les objets ne sont pas déjà itérables non plus. Si vous pouvez les polyfill comme ci-dessus, ou utilisezfor … in je ne vois pas d'argument simple.

Une suggestion possible est que ce qui est itérable est un type d'objet, il est donc possible qu'il ait été limité à un sous-ensemble d'objets juste au cas où d'autres objets exploseraient lors de la tentative.

Manngo
la source