web-dev-qa-db-fra.com

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

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

Je vois tout le temps des questions relatives à la répétition d'objets, la solution habituelle é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 instructions comme ES6 for...of seraient agréables à utiliser pour les objets par défaut. Comme ces fonctionnalités ne sont disponibles que pour des "objets itérables" spéciaux qui n'incluent pas les objets {}, Nous devons passer en revue les étapes pour que cela fonctionne pour les objets pour lesquels nous souhaitons l'utiliser.

L'instruction for ... of crée une boucle Itérant sur objets itératifs (y compris Array, Map, Set, objet arguments, etc.) ...

Par exemple, en utilisant un ES6 fonction du générateur :

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 que j’attends lorsque j’exécute le code dans Firefox (qui prend en charge ES6 ):

output of hacky for...of

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

De plus, les objets {} Étant différents des collections "similaires à un tableau" et des "objets itératifs" tels que NodeList , HtmlCollection , et 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 ci-dessus, j'aimerais beaucoup voir un exemple concret sur la manière de transformer des objets {} En éléments itérables, en particulier de ceux qui ont mentionné le [Symbol.iterator]. Ceci devrait permettre à ces nouveaux "objets itératifs" {} D'utiliser des instructions telles que for...of. De plus, je me demande si le fait de rendre des objets itérables permet de les convertir en tableaux.

J'ai essayé le code ci-dessous, mais j'ai 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
66
boombox

Je vais essayer ceci. 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 de manière définitive pourquoi ils ont ou n'ont rien fait. Cependant, je vais exposer mes hypothèses et tirer le meilleur parti.

1. Pourquoi ajouter une construction for...of En premier lieu?

JavaScript inclut déjà une construction for...in Qui peut être utilisée pour itérer les propriétés d'un objet. Cependant, il s'agit de pas vraiment une boucle forEach , car il é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 tombe en panne dans des cas plus complexes (y compris avec des tableaux, où son utilisation a tendance à être soit découragée ou complètement obscurcie par les garanties nécessaires pour utiliser for...in Avec un tableau correctement). Vous pouvez contourner ce problème en utilisant hasOwnProperty (entre autres), mais c'est un peu maladroit et inélégant.

Par conséquent, je pars du principe que la construction for...of Est ajoutée pour remédier aux carences associées à la construction for...in Et pour offrir une plus grande utilité et une plus grande souplesse lors des itérations. Les gens ont tendance à traiter for...in Comme une boucle forEach pouvant être généralement appliquée à n'importe quelle collection et produire des résultats sains dans tous les contextes possibles, mais ce n'est pas ce qui se produit. La boucle for...of Résout ce problème.

Je suppose également qu'il est important que le code ES5 existant s'exécute sous ES6 et produise le même résultat que sous ES5. Par conséquent, il est impossible de modifier de manière décisive le comportement de la construction for...in.

2. Comment fonctionne for...of?

Le documentation de référence est utile pour cette partie. Plus précisément, un objet est considéré comme iterable s'il définit la propriété Symbol.iterator.

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 plus d'éléments à extraire. Des implémentations prédéfinies sont fournies pour certains types d'objets , et il est relativement clair que l'utilisation de for...of Ne fait que déléguer à la fonction itérateur.

Cette approche est utile car il est très simple de fournir vos propres itérateurs. Je pourrais dire que l’approche aurait pu poser des problèmes pratiques car elle dépendait de la définition d’une propriété alors qu’il n’y en avait aucune auparavant, à l’exception 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 le cherchiez délibérément (c.-à-d. il ne présentera pas dans les boucles for...in sous forme de clé, etc.). Donc ce n'est pas le cas.

En dehors des 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 iterable utilisant for...of Par défaut?

Mon suppose est qu'il s'agit d'une combinaison de:

  1. Rendre tous les objets iterable par défaut peut avoir été considéré comme inacceptable car il ajoute une propriété là où il n'en existait aucune auparavant, ou parce qu'un objet n'est pas (nécessairement) une collection. Comme Felix le note, "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 de for...in, Et il n'est pas clair ce qu'une implémentation intégrée d'itérateur aurait pu faire différemment/mieux que le comportement existant for...in. Donc, même si # 1 est faux et que l'ajout de la propriété était acceptable, il n'a peut-être pas été vu comme utile.
  3. Les utilisateurs qui souhaitent créer leurs objets iterable peuvent le faire facilement en définissant la propriété Symbol.iterator.
  4. La spécification ES6 fournit également un type Map , qui estiterable par défaut et présente quelques autres petits avantages par rapport à l'utilisation d'un objet brut en tant que Map.

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

Etant donné que les objets peuvent être facilement créés iterable, qu’ils peuvent déjà être itérés avec for...in, Et qu’il n’ya probablement pas d’accord clair sur ce qu’un itérateur par défaut devrait faire (si son objet est voulu) pour être quelque peu différent de ce que for...in fait), il semble assez raisonnable que les objets n’aient pas été créés par défaut iterable.

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 aussi créer votre objet iterable comme vous le souhaitez en effectuant quelque chose comme ceci (ou vous pouvez créer tout objets iterable en assigner à Object.prototype[Symbol.iterator] à la 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 (sous Chrome, à bail): http://jsfiddle.net/rncr3ppz/5/

Modifier

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

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

var array = [...myIterable];
31
aroth

Objects n'implémente pas les protocoles d'itération en Javascript pour de très bonnes raisons. Il existe deux niveaux auxquels les propriétés d'objet peuvent être itérées en 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éflexive. 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. Comme les tableaux stockent des séquences de données, nous nous intéressons régulièrement au niveau de données uniquement. for..in n'a évidemment aucun sens en ce qui concerne les tableaux et autres structures "orientées données" dans la plupart des cas. C’est la raison pour laquelle ES2015 a introduit for..of et le protocole itérable.

Itération au niveau des données

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

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

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

  • [].length par exemple est un Number mais 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 des objets Javascript simples et vieux dépendrait du niveau de données. Mais comme nous venons de le voir, une distinction fiable entre l'itération des données et celle 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 descripteur enumerable. Mais est-il vraiment conseillé de s’en remettre à cela? Je crois que non! La signification du descripteur enumerable est trop floue.

Conclusion

Il n'y a pas de moyen significatif d'implémenter les protocoles d'itération pour les objets, car chaque objet n'est pas une collection.

Si les propriétés de l'objet étaient éditables par défaut, les niveaux de programme et de données étaient mélangés. Etant donné que chaque type composite en Javascript est basé sur des objets simples, ceci s'appliquerait également à Array et Map.

for..in, Object.keys, Reflect.ownKeys etc. peuvent être utilisés à la fois pour la réflexion et l’itération de données, une distinction claire n’est généralement pas possible. Si vous ne faites pas attention, vous vous retrouvez rapidement avec de la méta-programmation et des dépendances étranges. Le type de données abstrait Map met fin à la fusion des niveaux de programme et de données. Je pense que Map est la réalisation la plus significative d'ES2015, même si Promises sont beaucoup plus excitants.

7
user6445533

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

Ajouter l'itération aux objets eux-mêmes pourrait avoir des conséquences imprévues, 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
7
user663031

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]];
            }
        }
    }
});
4
Jack Slocum

Ceci est 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);
}

Oui entries est maintenant une méthode qui fait partie de Object :)

modifier

Après avoir examiné plus en profondeur, 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

De retour à votre exemple initial, si vous vouliez utiliser la méthode du prototype ci-dessus, il aimerait aimer ceci

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);
    }
}
1
Chad Scira

Techniquement, ceci 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 tout à fait aussi pratique qu’elle aurait dû être, mais c’est faisable, 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 comme tout le monde a droit à un avis, je ne vois pas pourquoi les objets ne sont pas déjà itérables non plus. Si vous pouvez les remplir comme ci-dessus, ou utilisez for … in alors je ne peux pas voir un argument simple.

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

0
Manngo