Warum können Objekte in JavaScript nicht iteriert werden?


87

Warum sind Objekte standardmäßig nicht iterierbar?

Ich sehe ständig Fragen im Zusammenhang mit iterierenden Objekten. Die übliche Lösung besteht darin, die Eigenschaften eines Objekts zu durchlaufen und auf diese Weise auf die Werte innerhalb eines Objekts zuzugreifen. Dies scheint so häufig zu sein, dass ich mich frage, warum Objekte selbst nicht iterierbar sind.

Anweisungen wie die ES6 sind for...ofstandardmäßig für Objekte geeignet. Da diese Funktionen nur für spezielle "iterierbare Objekte" verfügbar sind {}, die keine Objekte enthalten, müssen wir die Rahmen durchlaufen, damit dies für Objekte funktioniert, für die wir es verwenden möchten.

Die for ... of-Anweisung erstellt eine Schleife, die über iterierbare Objekte (einschließlich Array, Map, Set, Argumente, Objekt usw.) iteriert.

Zum Beispiel mit einer ES6- Generatorfunktion :

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

Das oben Gesagte protokolliert die Daten ordnungsgemäß in der Reihenfolge, in der ich sie erwarte, wenn ich den Code in Firefox (das ES6 unterstützt ) ausführe :

Ausgabe von Hacky für ... von

Standardmäßig sind {}Objekte nicht iterierbar, aber warum? Würden die Nachteile die potenziellen Vorteile von iterierbaren Objekten überwiegen? Was sind die damit verbundenen Probleme?

Darüber hinaus, weil {}Objekte unterscheiden sich von „Array-like“ Sammlungen sind und „iterable Objekte“ wie NodeList, HtmlCollectionund argumentskönnen sie nicht in Arrays umgewandelt werden.

Zum Beispiel:

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

oder mit Array-Methoden verwendet werden:

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

Neben den Fragen, die ich oben habe, würde ich gerne ein funktionierendes Beispiel sehen, wie {}Objekte zu iterablen Elementen gemacht werden können, insbesondere von denen, die das erwähnt haben [Symbol.iterator]. Dies sollte es diesen neuen {}"iterierbaren Objekten" ermöglichen, Anweisungen wie zu verwenden for...of. Ich frage mich auch, ob die iterierbare Darstellung von Objekten die Konvertierung in Arrays ermöglicht.

Ich habe den folgenden Code ausprobiert, aber ich bekomme einen 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

1
Alles, was eine Symbol.iteratorEigenschaft hat, ist iterierbar. Sie müssten also nur diese Eigenschaft implementieren. Eine mögliche Erklärung dafür, warum Objekte nicht iterierbar sind, könnte sein, dass dies implizieren würde, dass alles iterierbar ist, da alles ein Objekt ist (außer natürlich Primitiven). Was bedeutet es jedoch, über eine Funktion oder ein Objekt mit regulären Ausdrücken zu iterieren?
Felix Kling

7
Was ist deine eigentliche Frage hier? Warum hat die ECMA die Entscheidungen getroffen, die sie getroffen hat?
Steve Bennett

3
Da Objekte KEINE garantierte Reihenfolge ihrer Eigenschaften haben, frage ich mich, ob dies von der Definition einer Iterierbarkeit abweicht, für die Sie eine vorhersehbare Reihenfolge erwarten würden.
jfriend00

2
Um eine maßgebliche Antwort für "warum" zu erhalten, sollten Sie bei esdiscuss.org
Felix Kling

1
@FelixKling - geht es in diesem Beitrag um ES6? Sie sollten es wahrscheinlich bearbeiten, um zu sagen, um welche Version es sich handelt, da die "kommende Version von ECMAScript" im Laufe der Zeit nicht sehr gut funktioniert.
jfriend00

Antworten:


42

Ich werde es versuchen. Beachten Sie, dass ich nicht mit ECMA verbunden bin und keinen Einblick in deren Entscheidungsprozess habe. Daher kann ich nicht definitiv sagen, warum sie etwas getan haben oder nicht. Ich werde jedoch meine Annahmen darlegen und mein Bestes geben.

1. Warum überhaupt ein for...ofKonstrukt hinzufügen ?

JavaScript enthält bereits ein for...inKonstrukt, mit dem die Eigenschaften eines Objekts iteriert werden können. Es ist jedoch nicht wirklich eine forEach-Schleife , da sie alle Eigenschaften eines Objekts auflistet und nur in einfachen Fällen vorhersehbar funktioniert.

Es bricht in komplexeren Fällen zusammen (auch bei Arrays, bei denen seine Verwendung durch die Sicherheitsvorkehrungen, die für die korrekte Verwendung mit einem Array erforderlich sind, entweder entmutigt oder gründlich verschleiert wird ). Sie können das umgehen, indem Sie (unter anderem) verwenden, aber das ist etwas klobig und unelegant. for...inhasOwnProperty

Daher gehe ich davon aus, dass das for...ofKonstrukt hinzugefügt wird, um die mit dem for...inKonstrukt verbundenen Mängel zu beheben und eine größere Nützlichkeit und Flexibilität beim Iterieren von Dingen bereitzustellen. Menschen neigen dazu, for...inals forEachSchleife zu behandeln , die im Allgemeinen auf jede Sammlung angewendet werden kann und in jedem möglichen Kontext vernünftige Ergebnisse liefert, aber das passiert nicht. Die for...ofSchleife behebt das.

Ich gehe auch davon aus, dass es wichtig ist, dass vorhandener ES5-Code unter ES6 ausgeführt wird und dasselbe Ergebnis wie unter ES5 liefert, sodass beispielsweise am Verhalten des for...inKonstrukts keine wesentlichen Änderungen vorgenommen werden können .

2. Wie funktioniert das for...of?

Die Referenzdokumentation ist für diesen Teil hilfreich. Insbesondere wird ein Objekt berücksichtigt, iterablewenn es die Symbol.iteratorEigenschaft definiert .

Die Eigenschaftsdefinition sollte eine Funktion sein, die die Elemente in der Auflistung einzeln zurückgibt und ein Flag setzt, das angibt, ob weitere Elemente abgerufen werden sollen oder nicht. Für einige Objekttypen werden vordefinierte Implementierungen bereitgestellt , und es ist relativ klar, dass die for...ofeinfache Delegierung an die Iteratorfunktion erfolgt.

Dieser Ansatz ist nützlich, da es sehr einfach ist, eigene Iteratoren bereitzustellen. Ich könnte sagen, dass der Ansatz aufgrund seiner Abhängigkeit von der Definition einer Eigenschaft, bei der es zuvor keine gab, praktische Probleme hätte aufwerfen können, es sei denn, ich kann sagen, dass dies nicht der Fall ist, da die neue Eigenschaft im Wesentlichen ignoriert wird, es sei denn, Sie suchen absichtlich danach (d. H. es wird nicht in for...inSchleifen als Schlüssel usw. angezeigt). Das ist also nicht der Fall.

Abgesehen von praktischen Problemen kann es als konzeptionell umstritten angesehen worden sein, alle Objekte mit einer neuen vordefinierten Eigenschaft zu beginnen oder implizit zu sagen, dass "jedes Objekt eine Sammlung ist".

3. Warum werden die Objekte nicht iterablemit for...ofstandardmäßig?

Ich vermute, dass dies eine Kombination aus:

  1. Das iterablestandardmäßige Festlegen aller Objekte wurde möglicherweise als inakzeptabel angesehen, da eine Eigenschaft hinzugefügt wird, in der zuvor keine vorhanden war, oder weil ein Objekt (nicht unbedingt) eine Sammlung ist. Wie Felix bemerkt, "was bedeutet es, über eine Funktion oder ein Objekt mit regulären Ausdrücken zu iterieren"?
  2. Einfache Objekte können bereits mit iteriert werden for...in, und es ist nicht klar, was eine integrierte Iterator-Implementierung anders / besser als das vorhandene for...inVerhalten hätte tun können. Selbst wenn # 1 falsch ist und das Hinzufügen der Eigenschaft akzeptabel war, wurde dies möglicherweise nicht als nützlich angesehen .
  3. Benutzer, die ihre Objekte erstellen möchten, iterablekönnen dies einfach tun, indem sie die Symbol.iteratorEigenschaft definieren.
  4. Die ES6 spec bietet auch eine Map - Typ, der ist iterable standardmäßig aktiviert und hat einige andere kleine Vorteile gegenüber einem einfachen Objekt als eine Verwendung Map.

In der Referenzdokumentation finden Sie sogar ein Beispiel für Nummer 3:

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

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

Angesichts der Tatsache, dass Objekte leicht erstellt werden können iterable, dass sie bereits mit iteriert werden for...inkönnen und dass es wahrscheinlich keine klare Übereinstimmung darüber gibt, was ein Standardobjekt-Iterator tun soll (wenn das, was er tut, irgendwie anders sein soll als das, was er for...intut), erscheint es vernünftig genug, dass Objekte nicht iterablestandardmäßig erstellt wurden.

Beachten Sie, dass Ihr Beispielcode wie folgt umgeschrieben werden kann 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"
    }
}

... oder Sie können Ihr Objekt auch so erstellen, iterablewie Sie es möchten, indem Sie Folgendes tun (oder Sie können alle Objekte iterableerstellen, indem Sie sie Object.prototype[Symbol.iterator]stattdessen zuweisen ):

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

Damit können Sie hier spielen (zumindest in Chrome): http://jsfiddle.net/rncr3ppz/5/

Bearbeiten

Und als Antwort auf Ihre aktualisierte Frage ist es iterablemit dem Spread-Operator in ES6 möglich, ein in ein Array zu konvertieren .

Dies scheint jedoch noch nicht in Chrome zu funktionieren, oder zumindest kann ich es in meiner jsFiddle nicht zum Laufen bringen. Theoretisch sollte es so einfach sein wie:

var array = [...myIterable];

Warum nicht einfach obj[Symbol.iterator] = obj[Symbol.enumerate]in Ihrem letzten Beispiel?
Bergi

@Bergi - Weil ich das in den Dokumenten nicht gesehen habe (und diese hier beschriebene Eigenschaft nicht sehe ). Ein Argument für die explizite Definition des Iterators ist jedoch, dass es einfach ist, eine bestimmte Iterationsreihenfolge durchzusetzen, falls dies erforderlich sein sollte. Wenn die Iterationsreihenfolge nicht wichtig ist (oder wenn die Standardreihenfolge in Ordnung ist) und die einzeilige Verknüpfung funktioniert, gibt es wenig Grund, den prägnanteren Ansatz nicht zu wählen.
Aroth

Hoppla, [[enumerate]]ist kein bekanntes Symbol (@@ enumerate), sondern eine interne Methode. Ich müsste seinobj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
Bergi

Was nützen all diese Vermutungen, wenn der eigentliche Diskussionsprozess gut dokumentiert ist? Es ist sehr seltsam, dass Sie sagen würden: "Daher gehe ich davon aus, dass das Konstrukt for ... hinzugefügt wird, um die mit dem Konstrukt for ... verbundenen Mängel zu beheben." Nein. Es wurde hinzugefügt, um eine allgemeine Methode zum Iterieren über alles zu unterstützen, und ist Teil einer Vielzahl neuer Funktionen, einschließlich Iterables selbst, Generatoren sowie Karten und Sets. Es ist kaum als Ersatz oder Upgrade gedacht for...in, was einen anderen Zweck hat - die Eigenschaften eines Objekts zu durchlaufen .

2
Ein guter Punkt, der noch einmal betont, dass nicht jedes Objekt eine Sammlung ist. Objekte werden seit langem als solche verwendet, weil es sehr praktisch war, aber letztendlich sind sie nicht wirklich Sammlungen. Das haben wir Mapjetzt.
Felix Kling

9

Objects Implementieren Sie die Iterationsprotokolle aus sehr guten Gründen nicht in Javascript. Es gibt zwei Ebenen, auf denen Objekteigenschaften in JavaScript wiederholt werden können:

  • die Programmebene
  • die Datenebene

Iteration auf Programmebene

Wenn Sie ein Objekt auf Programmebene durchlaufen, untersuchen Sie einen Teil der Struktur Ihres Programms. Es ist eine reflektierende Operation. Lassen Sie uns diese Anweisung mit einem Array-Typ veranschaulichen, der normalerweise auf Datenebene wiederholt wird:

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

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

Wir haben gerade die Programmebene von untersucht xs. Da Arrays Datensequenzen speichern, interessieren wir uns regelmäßig nur für die Datenebene. for..inOffensichtlich macht dies in Verbindung mit Arrays und anderen "datenorientierten" Strukturen in den meisten Fällen keinen Sinn. Dies ist der Grund, warum ES2015 for..ofdas iterierbare Protokoll eingeführt hat.

Iteration auf Datenebene

Bedeutet das, dass wir die Daten einfach von der Programmebene unterscheiden können, indem wir Funktionen von primitiven Typen unterscheiden? Nein, da Funktionen auch Daten in Javascript sein können:

  • Array.prototype.sort erwartet beispielsweise, dass eine Funktion einen bestimmten Sortieralgorithmus ausführt
  • Thunks wie () => 1 + 2sind nur funktionale Wrapper für träge ausgewertete Werte

Neben primitiven Werten kann auch die Programmebene dargestellt werden:

  • [].lengthZum Beispiel ist a Numberaber repräsentiert die Länge eines Arrays und gehört somit zur Programmdomäne

Das bedeutet, dass wir die Programm- und Datenebene nicht durch bloße Überprüfung der Typen unterscheiden können.


Es ist wichtig zu verstehen, dass die Implementierung der Iterationsprotokolle für einfache alte Javascript-Objekte von der Datenebene abhängt. Wie wir gerade gesehen haben, ist eine zuverlässige Unterscheidung zwischen Iteration auf Daten- und Programmebene nicht möglich.

Mit Arrays ist diese Unterscheidung trivial: Jedes Element mit einem ganzzahligen Schlüssel ist ein Datenelement. Objects haben eine äquivalente Funktion: Der enumerableDeskriptor. Aber ist es wirklich ratsam, sich darauf zu verlassen? Ich glaube es ist nicht! Die Bedeutung des enumerableDeskriptors ist zu verschwommen.

Fazit

Es gibt keine sinnvolle Möglichkeit, die Iterationsprotokolle für Objekte zu implementieren, da nicht jedes Objekt eine Sammlung ist.

Wenn Objekteigenschaften standardmäßig iterierbar waren, wurden Programm- und Datenebene verwechselt. Da jeder zusammengesetzte Typ in Javascript auf einfachen Objekten basiert, würde dies auch für Arrayund gelten Map.

for..in, Object.keys, Reflect.ownKeysUsw. kann sowohl für Reflexion und Daten Iteration, eine klare Unterscheidung verwendet wird , ist regelmäßig nicht möglich. Wenn Sie nicht aufpassen, erhalten Sie schnell Metaprogrammierung und seltsame Abhängigkeiten. Der Mapabstrakte Datentyp beendet effektiv die Verschmelzung von Programm- und Datenebene. Ich glaube, dies Mapist die bedeutendste Errungenschaft in ES2015, auch wenn Promises viel aufregender sind.


3
+1, ich denke "Es gibt keine sinnvolle Möglichkeit, die Iterationsprotokolle für Objekte zu implementieren, da nicht jedes Objekt eine Sammlung ist." fasst es zusammen.
Charlie Schliesser

1
Ich denke nicht, dass das ein gutes Argument ist. Wenn Ihr Objekt keine Sammlung ist, warum versuchen Sie dann, eine Schleife darüber zu erstellen? Es spielt keine Rolle, dass nicht jedes Objekt eine Sammlung ist, da Sie nicht versuchen werden, über diejenigen zu iterieren, die es nicht sind.
BT

Eigentlich jedes Objekt ist eine Sammlung, und es ist nicht bis zu der Sprache , um zu entscheiden , ob die Sammlung kohärent ist. Arrays und Maps können auch nicht verwandte Werte erfassen. Der Punkt ist, dass Sie die Schlüssel jedes Objekts unabhängig von ihrer Verwendung durchlaufen können, sodass Sie nur einen Schritt davon entfernt sind, ihre Werte zu durchlaufen. Wenn Sie über eine Sprache sprechen, in der Array-Werte (oder andere Sammlungswerte) statisch eingegeben werden, können Sie über solche Einschränkungen sprechen, jedoch nicht über JavaScript.
Manngo

Das Argument, dass jedes Objekt keine Sammlung ist, macht keinen Sinn. Sie gehen davon aus, dass ein Iterator nur einen Zweck hat (eine Sammlung zu iterieren). Der Standarditerator für ein Objekt ist ein Iterator für die Eigenschaften des Objekts, unabhängig davon, was diese Eigenschaften darstellen (ob eine Sammlung oder etwas anderes). Wie Manngo sagte, wenn Ihr Objekt keine Sammlung darstellt, ist es Sache des Programmierers, es nicht wie eine Sammlung zu behandeln. Vielleicht möchten sie nur die Eigenschaften des Objekts für eine Debug-Ausgabe iterieren? Neben einer Sammlung gibt es noch viele andere Gründe.
jfriend00

8

Ich denke, die Frage sollte lauten: "Warum gibt es keine integrierte Objektiteration?"

Das Hinzufügen von Iterierbarkeit zu Objekten selbst könnte möglicherweise unbeabsichtigte Konsequenzen haben, und nein, es gibt keine Möglichkeit, die Reihenfolge zu garantieren, aber das Schreiben eines Iterators ist so einfach wie

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

Dann

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

a 1
b 2

1
danke torazaburo. Ich habe meine Frage überarbeitet. Ich würde gerne ein Beispiel sehen, [Symbol.iterator]in dem Sie diese unbeabsichtigten Konsequenzen erläutern können.
Boombox

4

Sie können alle Objekte einfach global iterierbar machen:

Object.defineProperty(Object.prototype, Symbol.iterator, {
    enumerable: false,
    value: function * (){
        for(let key in this){
            if(this.hasOwnProperty(key)){
                yield [key, this[key]];
            }
        }
    }
});

3
Fügen Sie nativen Objekten keine Methoden global hinzu. Dies ist eine schreckliche Idee, die Sie und jeden, der Ihren Code verwendet, in den Arsch beißen wird.
BT

2

Dies ist der neueste Ansatz (der in Chromkanarien funktioniert)

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

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

Ja entriesist jetzt eine Methode, die Teil von Object ist :)

bearbeiten

Nachdem Sie sich mehr damit befasst haben, scheint es, als könnten Sie Folgendes tun

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

Sie können dies jetzt tun

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

edit2

Zurück zu Ihrem ursprünglichen Beispiel, wenn Sie die obige Prototypmethode verwenden möchten, würde es Ihnen gefallen

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

2

Diese Frage hat mich auch gestört.

Dann kam mir die Idee Object.entries({...}), es zu verwenden , es gibt ein zurück, Arraydas ein istIterable .

Auch Dr. Axel Rauschmayer hat darauf eine hervorragende Antwort gegeben. Siehe Warum einfache Objekte NICHT iterierbar sind


0

Technisch gesehen ist dies keine Antwort auf die Frage warum? Aber ich habe Jack Slocums Antwort oben im Lichte von BTs Kommentaren an etwas angepasst, das verwendet werden kann, um ein Objekt iterierbar zu machen.

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

Nicht ganz so praktisch, wie es hätte sein sollen, aber es funktioniert, insbesondere wenn Sie mehrere Objekte haben:

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

Und weil jeder zu einer Meinung berechtigt ist, kann ich nicht verstehen, warum Objekte auch nicht bereits iterierbar sind. Wenn Sie sie wie oben polyfillieren oder verwenden könnenfor … in können, sehe ich kein einfaches Argument.

Ein möglicher Vorschlag ist , dass das, was iterable a Art von Objekt, so dass es möglich ist , dass iterable einige andere Objekte auf eine Teilmenge von Objekten für alle Fälle beschränkt wurde bei dem Versuch explodieren.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.