JavaScript-Schleifen und Objektiterationen: Das große Verwirrspiel

Max Schneider
Max Schneider ·
Max

Die Verwirrung beim Durchlaufen von Objekten in JavaScript

Als ich neulich JavaScript-Code schrieb, stieß ich auf ein seltsames Verhalten: Warum gibt eine for...in-Schleife bei Maps nichts aus, während eine for...of-Schleife problemlos funktioniert? Und andererseits: Warum zeigt eine for...in-Schleife bei Arrays zusätzliche Eigenschaften an, die for...of ignoriert?

Dieses scheinbar inkonsistente Verhalten führt zu Verwirrung. In diesem Blogpost werden wir genau untersuchen, was hinter diesen Unterschieden steckt und warum JavaScript so funktioniert, wie es funktioniert.

Das Map-Rätsel: Warum for...in nichts ausgibt

Beginnen wir mit einer Map:

const names = new Map();
names.set("Tim", 2).set("Max", 25);
console.log(names); // Map(2) { 'Tim' => 2, 'Max' => 25 }
 
// Mit for...in - gibt nichts aus!
for (let name in names.keys()) {
  console.log(name);
}
// Keine Ausgabe!
 
// Mit for...of - funktioniert wie erwartet
for (let name of names.keys()) {
  console.log(name);
}
// Ausgabe:
// Tim
// Max

Warum verhält es sich so? Die Antwort liegt in der grundlegenden Natur von JavaScript-Objekten und Iteratoren.

Die Erklärung:

  1. for...in durchläuft enumerable properties:

    • Diese Schleife wurde für reguläre Objekte ({}) konzipiert.
    • Sie iteriert über alle aufzählbaren Eigenschaften eines Objekts (einschließlich geerbter Eigenschaften).
    • Map-Objekte und der Rückgabewert von keys() haben keine aufzählbaren Eigenschaften in diesem Sinne.
  2. for...of verwendet das Iteration Protocol:

    • Diese Schleife wurde speziell für iterierbare Objekte entworfen.
    • Map, Set, Array und andere Strukturen implementieren das sogenannte Iteration Protocol.
    • Map.prototype.keys() gibt ein iterierbares Objekt zurück, das mit for...of funktioniert.

Das Array-Paradoxon: Warum zeigt for...in mehr an als for...of?

Wechseln wir nun zu Arrays und sehen uns ein faszinierendes Beispiel an:

const numbers = [1, 2, 3, 4, 5, 6, 7];
numbers.name = "Max"; // Arrays sind Objekte und können um Eigenschaften erweitert werden
 
console.log(numbers); // [ 1, 2, 3, 4, 5, 6, 7, name: 'Max' ]
 
// Mit for...in
for (let i in numbers) {
  console.log(i);
}
// Ausgabe:
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// name
 
// Mit for...of
for (let i of numbers) {
  console.log(i);
}
// Ausgabe:
// 1
// 2
// 3
// 4
// 5
// 6
// 7

Verwirrend, oder? Die for...in-Schleife gibt sowohl die Indizes als auch die zusätzliche Eigenschaft name aus, während for...of nur die tatsächlichen Array-Elemente ausgibt.

Die Erklärung:

  1. Arrays in JavaScript sind besondere Objekte:

    • Arrays haben numerische Indizes (0, 1, 2, ...) als Eigenschaften.
    • Sie können auch beliebige andere Eigenschaften haben (wie jedes Objekt).
    • Die Länge eines Arrays wird nur durch die höchste numerische Indexposition bestimmt.
  2. Der Array-Iterator ist spezialisiert:

    • Der vordefinierte Iterator eines Arrays berücksichtigt nur die numerisch indizierten Elemente.
    • Er ignoriert absichtlich zusätzliche Eigenschaften wie name.
    • Dies entspricht der typischen Erwartung beim Arbeiten mit Arrays.
  3. Die Verwirrung entsteht durch die duale Natur von Arrays:

    • Arrays sind gleichzeitig Sammlungen von Werten (Elemente).
    • Arrays sind aber auch Objekte mit Eigenschaften.

Warum ist das so designt?

Diese Unterschiede können verwirrend erscheinen, aber sie haben sowohl historische als auch praktische Gründe:

Historische Gründe:

  • for...in ist eine ältere Schleife, die für einfache Objekte entwickelt wurde, bevor JavaScript ausgefeilte Datenstrukturen wie Maps hatte.
  • for...of wurde mit ECMAScript 2015 (ES6) eingeführt, als iterierbare Protokolle und spezielle Datenstrukturen Teil der Sprache wurden.

Praktische Gründe:

  • Bei Arrays möchte man typischerweise nur über die numerischen Elemente iterieren, nicht über Metadaten.
  • Bei Maps und Sets möchte man gezielt über Schlüssel, Werte oder Einträge iterieren.
  • Objekte haben keine natürliche Reihenfolge, daher macht eine for...in-Schleife Sinn.

Demonstration: Was passiert unter der Haube?

Um besser zu verstehen, was hier passiert, sehen wir uns an, wie Arrays und Maps strukturiert sind:

Arrays unter der Haube:

const arr = [1, 2, 3];
arr.name = "Mein Array";
 
// Konzeptuell ähnlich zu:
const arrLikeObj = {
  0: 1,
  1: 2,
  2: 3,
  name: "Mein Array",
  length: 3,
  // Plus spezielle Array-Methoden und der Iterator
};

Das Array hat sowohl numerische Indizes als auch zusätzliche Eigenschaften.

Maps unter der Haube:

const map = new Map();
map.set("a", 1).set("b", 2);
 
// Intern speichert die Map ihre Daten getrennt von ihren Eigenschaften
// Die Schlüssel-Wert-Paare sind NICHT als Eigenschaften zugänglich

Die Map speichert ihre Einträge in einer internen Struktur, die nicht als Eigenschaften des Map-Objekts selbst zugänglich sind.

Ist das bei anderen Programmiersprachen auch so?

JavaScript ist in dieser Hinsicht ziemlich einzigartig. In vielen anderen Sprachen wäre dieses Verhalten gar nicht möglich:

  • Java, C#, C++: Arrays haben eine feste Struktur und können nicht um beliebige Eigenschaften erweitert werden.
  • Python: Listen können keine benutzerdefinierten Eigenschaften haben.
  • Ruby: Arrays sind Objekte, aber du kannst nicht einfach neue Eigenschaften hinzufügen.

Einige Sprachen haben ähnliche Konzepte:

  • PHP: Arrays können sowohl numerische als auch assoziative Schlüssel haben.
  • Lua: Tabellen können als Array und Objekt gleichzeitig fungieren.

Ein tieferer Blick auf das Iteration Protocol

Was JavaScript besonders macht, ist das "Iteration Protocol". Es besteht aus zwei Teilen:

  1. Iterable Protocol: Ein Objekt ist iterierbar, wenn es eine Methode mit dem Symbol Symbol.iterator implementiert.
  2. Iterator Protocol: Ein Iterator ist ein Objekt mit einer next()-Methode, die ein Objekt mit den Eigenschaften value und done zurückgibt.

Arrays, Maps, Sets und andere eingebaute Sammlungstypen implementieren dieses Protokoll:

// Manuelle Verwendung des Iterators eines Arrays
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
 
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

for...of nutzt diesen Iterator automatisch, während for...in es nicht tut.

Zusammenfassung

Die unterschiedlichen Verhaltensweisen von for...in und for...of in JavaScript sind nicht inkonsistent, sondern reflektieren die verschiedenen Anwendungsfälle und die historische Entwicklung der Sprache:

  • for...in: Durchläuft aufzählbare Eigenschaften eines Objekts (einschließlich Array-Indizes).
  • for...of: Durchläuft die Werte, die ein Iterator eines iterierbaren Objekts liefert.

Diese Unterscheidung ermöglicht mehr Flexibilität bei der Arbeit mit verschiedenen Datenstrukturen, auch wenn sie anfangs verwirrend wirken kann. Mit der Zeit wird dieses Konzept jedoch klarer, besonders wenn man mit verschiedenen Arten von Datenstrukturen arbeitet.

Denk daran: JavaScript ist eine Sprache, die sich über viele Jahre entwickelt hat, und manche ihrer Eigenheiten sind das Ergebnis dieser Evolution. Was heute verwirrend erscheint, hatte zu seiner Zeit oft gute Gründe.

Interaktives Beispiel: Probier es selbst aus!

Hier ist ein Code-Snippet, das du in deiner Browser-Konsole ausprobieren kannst, um die Unterschiede zu sehen:

// Erstelle verschiedene Datentypen
const myArray = [10, 20, 30];
myArray.customProp = "Hallo";
 
const myObject = { a: 1, b: 2, c: 3 };
 
const myMap = new Map();
myMap.set("eins", 1).set("zwei", 2);
 
// Experimentiere mit for...in
console.log("--- for...in mit Array ---");
for (let key in myArray) console.log(key);
 
console.log("--- for...in mit Object ---");
for (let key in myObject) console.log(key);
 
console.log("--- for...in mit Map ---");
for (let key in myMap) console.log(key);
 
// Experimentiere mit for...of
console.log("--- for...of mit Array ---");
for (let value of myArray) console.log(value);
 
console.log("--- for...of mit Object --- (Fehler!)");
try {
  for (let value of myObject) console.log(value);
} catch (e) {
  console.log("Fehler: " + e.message);
}
 
console.log("--- for...of mit Map ---");
for (let entry of myMap) console.log(entry);

Dies wird dir zeigen, wie sich die verschiedenen Schleifen mit den verschiedenen Datentypen verhalten, und dir helfen, ein besseres Verständnis zu entwickeln.

Viel Spaß beim Experimentieren und danke fürs Lesen!

Max

Hallo 👋 Ich bin Max dein freiberuflicher Softwareentwickler und Author des Blogs. Du kannst meine Arbeit auf Social Media verfolgen.

Mehr Blogbeiträge