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...of
standardmäß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 :
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
, HtmlCollection
und arguments
kö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
Symbol.iterator
Eigenschaft 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?Antworten:
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...of
Konstrukt hinzufügen ?JavaScript enthält bereits ein
for...in
Konstrukt, 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...in
hasOwnProperty
Daher gehe ich davon aus, dass das
for...of
Konstrukt hinzugefügt wird, um die mit demfor...in
Konstrukt verbundenen Mängel zu beheben und eine größere Nützlichkeit und Flexibilität beim Iterieren von Dingen bereitzustellen. Menschen neigen dazu,for...in
alsforEach
Schleife 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. Diefor...of
Schleife 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...in
Konstrukts 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,
iterable
wenn es dieSymbol.iterator
Eigenschaft 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...of
einfache 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...in
Schleifen 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
iterable
mitfor...of
standardmäßig?Ich vermute, dass dies eine Kombination aus:
iterable
standardmäß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"?for...in
, und es ist nicht klar, was eine integrierte Iterator-Implementierung anders / besser als das vorhandenefor...in
Verhalten 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 .iterable
können dies einfach tun, indem sie dieSymbol.iterator
Eigenschaft definieren.iterable
standardmäßig aktiviert und hat einige andere kleine Vorteile gegenüber einem einfachen Objekt als eine VerwendungMap
.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 werdenfor...in
kö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 erfor...in
tut), erscheint es vernünftig genug, dass Objekte nichtiterable
standardmäß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,
iterable
wie Sie es möchten, indem Sie Folgendes tun (oder Sie können alle Objekteiterable
erstellen, indem Sie sieObject.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
iterable
mit 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];
quelle
obj[Symbol.iterator] = obj[Symbol.enumerate]
in Ihrem letzten Beispiel?[[enumerate]]
ist kein bekanntes Symbol (@@ enumerate), sondern eine interne Methode. Ich müsste seinobj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
for...in
, was einen anderen Zweck hat - die Eigenschaften eines Objekts zu durchlaufen .Map
jetzt.Object
s 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: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..in
Offensichtlich macht dies in Verbindung mit Arrays und anderen "datenorientierten" Strukturen in den meisten Fällen keinen Sinn. Dies ist der Grund, warum ES2015for..of
das 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() => 1 + 2
sind nur funktionale Wrapper für träge ausgewertete WerteNeben primitiven Werten kann auch die Programmebene dargestellt werden:
[].length
Zum Beispiel ist aNumber
aber repräsentiert die Länge eines Arrays und gehört somit zur ProgrammdomäneDas 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
Array
s ist diese Unterscheidung trivial: Jedes Element mit einem ganzzahligen Schlüssel ist ein Datenelement.Object
s haben eine äquivalente Funktion: Derenumerable
Deskriptor. Aber ist es wirklich ratsam, sich darauf zu verlassen? Ich glaube es ist nicht! Die Bedeutung desenumerable
Deskriptors 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
Array
und geltenMap
.for..in
,Object.keys
,Reflect.ownKeys
Usw. 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. DerMap
abstrakte Datentyp beendet effektiv die Verschmelzung von Programm- und Datenebene. Ich glaube, diesMap
ist die bedeutendste Errungenschaft in ES2015, auch wennPromise
s viel aufregender sind.quelle
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
quelle
[Symbol.iterator]
in dem Sie diese unbeabsichtigten Konsequenzen erläutern können.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]]; } } } });
quelle
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
entries
ist 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); } }
quelle
Diese Frage hat mich auch gestört.
Dann kam mir die Idee
Object.entries({...})
, es zu verwenden , es gibt ein zurück,Array
das ein istIterable
.Auch Dr. Axel Rauschmayer hat darauf eine hervorragende Antwort gegeben. Siehe Warum einfache Objekte NICHT iterierbar sind
quelle
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önnen
for … 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.
quelle