JavaScript für ungeduldige Programmierer (ES2022-Ausgabe)
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

31 Arrays (Array)



31.1 Übersicht: Arrays

JavaScript Arrays sind eine sehr flexible Datenstruktur und werden als Listen, Stacks, Queues, Tupel (z. B. Paare) und mehr verwendet.

Einige Array-bezogene Operationen ändern Arrays destruktiv. Andere erzeugen nicht-destruktiv neue Arrays mit den Änderungen, die auf eine Kopie des ursprünglichen Inhalts angewendet werden.

31.1.1 Arrays verwenden

Ein Array erstellen, Elemente lesen und schreiben

// Creating an Array
const arr = ['a', 'b', 'c']; // Array literal
assert.deepEqual(
  arr,
  [ // Array literal
    'a',
    'b',
    'c', // trailing commas are ignored
  ]
);

// Reading elements
assert.equal(
  arr[0], 'a' // negative indices don’t work
);
assert.equal(
  arr.at(-1), 'c' // negative indices work
);

// Writing an element
arr[0] = 'x';
assert.deepEqual(
  arr, ['x', 'b', 'c']
);

Die Länge eines Arrays

const arr = ['a', 'b', 'c'];
assert.equal(
  arr.length, 3 // number of elements
);
arr.length = 1; // removing elements
assert.deepEqual(
  arr, ['a']
);
arr[arr.length] = 'b'; // adding an element
assert.deepEqual(
  arr, ['a', 'b']
);

Elemente destruktiv per .push() hinzufügen

const arr = ['a', 'b'];

arr.push('c'); // adding an element
assert.deepEqual(
  arr, ['a', 'b', 'c']
);

// Pushing Arrays (used as arguments via spreading (...)):
arr.push(...['d', 'e']);
assert.deepEqual(
  arr, ['a', 'b', 'c', 'd', 'e']
);

Elemente nicht-destruktiv per Spread-Syntax (...) hinzufügen

const arr1 = ['a', 'b'];
const arr2 = ['c'];
assert.deepEqual(
  [...arr1, ...arr2, 'd', 'e'],
  ['a', 'b', 'c', 'd', 'e']
);

Arrays leeren (alle Elemente entfernen)

// Destructive – affects everyone referring to the Array:
const arr1 = ['a', 'b', 'c'];
arr1.length = 0;
assert.deepEqual(
  arr1, []
);

// Non-destructive – does not affect others referring to the Array:
let arr2 = ['a', 'b', 'c'];
arr2 = [];
assert.deepEqual(
  arr2, []
);

Über Elemente iterieren

const arr = ['a', 'b', 'c'];
for (const value of arr) {
  console.log(value);
}

// Output:
// 'a'
// 'b'
// 'c'

Über Index-Wert-Paare iterieren

const arr = ['a', 'b', 'c'];
for (const [index, value] of arr.entries()) {
  console.log(index, value);
}

// Output:
// 0, 'a'
// 1, 'b'
// 2, 'c'

Arrays erstellen und füllen, wenn wir keine Array-Literale verwenden können (z. B. weil wir ihre Längen im Voraus nicht kennen oder sie zu groß sind)

const four = 4;

// Empty Array that we’ll fill later
assert.deepEqual(
  new Array(four),
  [ , , , ,] // four holes; last comma is ignored
);

// An Array filled with a primitive value
assert.deepEqual(
  new Array(four).fill(0),
  [0, 0, 0, 0]
);

// An Array filled with objects
// Why not .fill()? We’d get single object, shared multiple times.
assert.deepEqual(
  Array.from({length: four}, () => ({})),
  [{}, {}, {}, {}]
);

// A range of integers
assert.deepEqual(
  Array.from({length: four}, (_, i) => i),
  [0, 1, 2, 3]
);

31.1.2 Array-Methoden

Dieser Abschnitt gibt einen kurzen Überblick über die Array-API. Am Ende dieses Kapitels gibt es eine umfassendere Schnellreferenz.

Ein neues Array aus einem bestehenden Array ableiten

> ['■','●','▲'].slice(1, 3)
['●','▲']
> ['■','●','■'].filter(x => x==='■') 
['■','■']

> ['▲','●'].map(x => x+x)
['▲▲','●●']
> ['▲','●'].flatMap(x => [x,x])
['▲','▲','●','●']

Ein Array-Element an einem gegebenen Index entfernen

// .filter(): remove non-destructively
const arr1 = ['■','●','▲'];
assert.deepEqual(
  arr1.filter((_, index) => index !== 1),
  ['■','▲']
);
assert.deepEqual(
  arr1, ['■','●','▲'] // unchanged
);

// .splice(): remove destructively
const arr2 = ['■','●','▲'];
arr2.splice(1, 1); // start at 1, delete 1 element
assert.deepEqual(
  arr2, ['■','▲'] // changed
);

Eine Zusammenfassung eines Arrays berechnen

> ['■','●','▲'].some(x => x==='●')
true
> ['■','●','▲'].every(x => x==='●')
false

> ['■','●','▲'].join('-')
'■-●-▲'

> ['■','▲'].reduce((result,x) => result+x, '●')
'●■▲'
> ['■','▲'].reduceRight((result,x) => result+x, '●')
'●▲■'

Umkehren und Füllen

// .reverse() changes and returns `arr`
const arr = ['■','●','▲'];
assert.deepEqual(
  arr.reverse(), arr
);
// `arr` was changed:
assert.deepEqual(
  arr, ['▲','●','■']
);

// .fill() works the same way:
assert.deepEqual(
  ['■','●','▲'].fill('●'),
  ['●','●','●']
);

.sort() modifiziert ebenfalls ein Array und gibt es zurück

// By default, string representations of the Array elements
// are sorted lexicographically:
assert.deepEqual(
  [200, 3, 10].sort(),
  [10, 200, 3]
);

// Sorting can be customized via a callback:
assert.deepEqual(
  [200, 3, 10].sort((a,b) => a - b), // sort numerically
  [ 3, 10, 200 ]
);

Array-Elemente finden

> ['■','●','■'].includes('■')
true
> ['■','●','■'].indexOf('■')
0
> ['■','●','■'].lastIndexOf('■')
2
> ['■','●','■'].find(x => x==='■')
'■'
> ['■','●','■'].findIndex(x => x==='■')
0

Elemente am Anfang oder Ende hinzufügen oder entfernen

// Adding and removing at the start
const arr1 = ['■','●'];
arr1.unshift('▲');
assert.deepEqual(
  arr1, ['▲','■','●']
);
arr1.shift();
assert.deepEqual(
  arr1, ['■','●']
);

// Adding and removing at the end
const arr2 = ['■','●'];
arr2.push('▲');
assert.deepEqual(
  arr2, ['■','●','▲']
);
arr2.pop();
assert.deepEqual(
  arr2, ['■','●']
);

31.2 Die zwei Arten, Arrays in JavaScript zu verwenden

Es gibt zwei Arten, Arrays in JavaScript zu verwenden:

In der Praxis werden diese beiden Arten oft gemischt.

Insbesondere sind Sequenz-Arrays so flexibel, dass wir sie als (traditionelle) Arrays, Stacks und Queues verwenden können. Wie das geht, sehen wir später.

31.3 Grundlegende Array-Operationen

31.3.1 Arrays erstellen, lesen und schreiben

Der beste Weg, ein Array zu erstellen, ist über ein Array-Literal

const arr = ['a', 'b', 'c'];

Das Array-Literal beginnt und endet mit eckigen Klammern []. Es erstellt ein Array mit drei Elementen: 'a', 'b' und 'c'.

Nachgestellte Kommas sind in Array-Literalen erlaubt und werden ignoriert.

const arr = [
  'a',
  'b',
  'c',
];

Um ein Array-Element zu lesen, setzen wir einen Index in eckige Klammern (Indizes beginnen bei Null).

const arr = ['a', 'b', 'c'];
assert.equal(arr[0], 'a');

Um ein Array-Element zu ändern, weisen wir einem Array mit einem Index einen Wert zu.

const arr = ['a', 'b', 'c'];
arr[0] = 'x';
assert.deepEqual(arr, ['x', 'b', 'c']);

Der Bereich der Array-Indizes ist 32 Bit (ohne die maximale Länge): [0, 232−1).

31.3.2 Die .length-Eigenschaft eines Arrays

Jedes Array hat eine Eigenschaft .length, die sowohl die Anzahl der Elemente in einem Array liest als auch (!) ändert.

Die Länge eines Arrays ist immer der höchste Index plus eins.

> const arr = ['a', 'b'];
> arr.length
2

Wenn wir in das Array am Index der Länge schreiben, hängen wir ein Element an.

> arr[arr.length] = 'c';
> arr
[ 'a', 'b', 'c' ]
> arr.length
3

Eine weitere Möglichkeit, ein Element anzuhängen (destruktiv), ist über die Array-Methode .push().

> arr.push('d');
> arr
[ 'a', 'b', 'c', 'd' ]

Wenn wir .length setzen, kürzen wir das Array, indem wir Elemente entfernen.

> arr.length = 1;
> arr
[ 'a' ]

  Übung: Leere Zeilen mit .push() entfernen

exercises/arrays/remove_empty_lines_push_test.mjs

31.3.3 Zugriff auf Elemente über negative Indizes

Mehrere Array-Methoden unterstützen negative Indizes. Ist ein Index negativ, wird er zur Länge eines Arrays addiert, um einen nutzbaren Index zu erhalten. Daher sind die folgenden beiden Aufrufe von .slice() äquivalent: Sie kopieren beide arr ab dem letzten Element.

> const arr = ['a', 'b', 'c'];
> arr.slice(-1)
[ 'c' ]
> arr.slice(arr.length - 1)
[ 'c' ]
31.3.3.1 .at(): einzelne Elemente lesen (unterstützt negative Indizes) [ES2022]

Die Array-Methode .at() gibt das Element an einem gegebenen Index zurück. Sie unterstützt positive und negative Indizes (-1 bezieht sich auf das letzte Element, -2 auf das vorletzte Element usw.).

> ['a', 'b', 'c'].at(0)
'a'
> ['a', 'b', 'c'].at(-1)
'c'

Im Gegensatz dazu unterstützt der Klammeroperator [] keine negativen Indizes (und kann nicht geändert werden, da dies bestehenden Code brechen würde). Er interpretiert sie als Schlüssel von Nicht-Element-Properties.

const arr = ['a', 'b', 'c'];

arr[-1] = 'non-element property';
// The Array elements didn’t change:
assert.deepEqual(
  Array.from(arr), // copy just the Array elements
  ['a', 'b', 'c']
);

assert.equal(
  arr[-1], 'non-element property'
);

31.3.4 Arrays leeren

Um ein Array zu leeren (zu entleeren), können wir entweder seine .length auf null setzen

const arr = ['a', 'b', 'c'];
arr.length = 0;
assert.deepEqual(arr, []);

oder wir können der Variablen, die das Array speichert, ein neues leeres Array zuweisen.

let arr = ['a', 'b', 'c'];
arr = [];
assert.deepEqual(arr, []);

Der letztere Ansatz hat den Vorteil, dass er keine anderen Orte beeinflusst, die auf dasselbe Array zeigen. Wenn wir jedoch ein gemeinsam genutztes Array für alle zurücksetzen wollen, dann brauchen wir den ersteren Ansatz.

31.3.5 Spread-Syntax in Array-Literalen [ES6]

Innerhalb eines Array-Literals besteht ein Spread-Element aus drei Punkten (...) gefolgt von einem Ausdruck. Es ergibt sich, dass der Ausdruck ausgewertet und dann iteriert wird. Jeder iterierte Wert wird zu einem zusätzlichen Array-Element – zum Beispiel:

> const iterable = ['b', 'c'];
> ['a', ...iterable, 'd']
[ 'a', 'b', 'c', 'd' ]

Das bedeutet, dass wir mit Spread-Syntax eine Kopie eines Arrays erstellen und ein Iterable in ein Array umwandeln können.

const original = ['a', 'b', 'c'];

const copy = [...original];

const iterable = original.keys();
assert.deepEqual(
  [...iterable], [0, 1, 2]
);

Für beide vorherigen Anwendungsfälle finde ich Array.from() jedoch aussagekräftiger und bevorzuge es.

const copy2 = Array.from(original);

assert.deepEqual(
  Array.from(original.keys()), [0, 1, 2]
);

Spread-Syntax ist auch praktisch zum Verketten von Arrays (und anderen Iterables) zu Arrays.

const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];

const concatenated = [...arr1, ...arr2, 'e'];
assert.deepEqual(
  concatenated,
  ['a', 'b', 'c', 'd', 'e']);

Da Spread-Syntax Iteration verwendet, funktioniert sie nur, wenn der Wert iterierbar ist.

> [...'abc'] // strings are iterable
[ 'a', 'b', 'c' ]
> [...123]
TypeError: 123 is not iterable
> [...undefined]
TypeError: undefined is not iterable

  Spread-Syntax und Array.from() erzeugen flache Kopien

Das Kopieren von Arrays per Spread-Syntax oder per Array.from() ist flach: Wir erhalten neue Einträge in einem neuen Array, aber die Werte werden mit dem ursprünglichen Array geteilt. Die Folgen einer flachen Kopie werden in §28.4 „Spread-Syntax in Objekt-Literalen (...) [ES2018]“ gezeigt.

31.3.6 Arrays: Indizes und Einträge auflisten [ES6]

Die Methode .keys() listet die Indizes eines Arrays auf.

const arr = ['a', 'b'];
assert.deepEqual(
  Array.from(arr.keys()), // (A)
  [0, 1]);

.keys() gibt ein Iterable zurück. In Zeile A wandeln wir dieses Iterable in ein Array um.

Das Auflisten von Array-Indizes unterscheidet sich vom Auflisten von Properties. Ersteres erzeugt Zahlen; letzteres erzeugt (zusätzlich zu Nicht-Index-Property-Keys) String-ifizierte Zahlen.

const arr = ['a', 'b'];
arr.prop = true;

assert.deepEqual(
  Object.keys(arr),
  ['0', '1', 'prop']);

Die Methode .entries() listet den Inhalt eines Arrays als [Index, Element]-Paare auf.

const arr = ['a', 'b'];
assert.deepEqual(
  Array.from(arr.entries()),
  [[0, 'a'], [1, 'b']]);

31.3.7 Ist ein Wert ein Array?

Folgend sind zwei Möglichkeiten, zu überprüfen, ob ein Wert ein Array ist:

> [] instanceof Array
true
> Array.isArray([])
true

instanceof ist normalerweise in Ordnung. Wir benötigen Array.isArray(), wenn ein Wert aus einem anderen Realm stammen kann. Grob gesagt ist ein Realm eine Instanz des globalen Geltungsbereichs von JavaScript. Einige Realms sind voneinander isoliert (z. B. Web Workers in Browsern), aber es gibt auch Realms, zwischen denen wir Daten bewegen können – zum Beispiel Same-Origin-Iframes in Browsern. x instanceof Array prüft die Prototypenkette von x und gibt daher false zurück, wenn x ein Array aus einem anderen Realm ist.

typeof kategorisiert Arrays als Objekte.

> typeof []
'object'

31.4 for-of und Arrays [ES6]

Die for-of-Schleife haben wir bereits früher in diesem Buch behandelt. Dieser Abschnitt fasst kurz zusammen, wie sie für Arrays verwendet wird.

31.4.1 for-of: über Elemente iterieren

Die folgende for-of-Schleife iteriert über die Elemente eines Arrays.

for (const element of ['a', 'b']) {
  console.log(element);
}
// Output:
// 'a'
// 'b'

31.4.2 for-of: über Indizes iterieren

Diese for-of-Schleife iteriert über die Indizes eines Arrays.

for (const element of ['a', 'b'].keys()) {
  console.log(element);
}
// Output:
// 0
// 1

31.4.3 for-of: über [Index, Element]-Paare iterieren

Die folgende for-of-Schleife iteriert über [Index, Element]-Paare. Destrukturierung (später beschrieben) gibt uns eine bequeme Syntax für die Einrichtung von index und element im Kopf von for-of.

for (const [index, element] of ['a', 'b'].entries()) {
  console.log(index, element);
}
// Output:
// 0, 'a'
// 1, 'b'

31.5 Array-ähnliche Objekte

Manche Operationen, die mit Arrays funktionieren, benötigen nur das absolute Minimum: Werte müssen nur Array-ähnlich sein. Ein Array-ähnlicher Wert ist ein Objekt mit den folgenden Eigenschaften:

Zum Beispiel akzeptiert Array.from() Array-ähnliche Objekte und wandelt sie in Arrays um.

// If we omit .length, it is interpreted as 0
assert.deepEqual(
  Array.from({}),
  []);

assert.deepEqual(
  Array.from({length:2, 0:'a', 1:'b'}),
  [ 'a', 'b' ]);

Die TypeScript-Schnittstelle für Array-ähnliche Objekte ist:

interface ArrayLike<T> {
  length: number;
  [n: number]: T;
}

  Array-ähnliche Objekte sind in modernem JavaScript relativ selten

Array-ähnliche Objekte waren vor ES6 üblich; heute sehen wir sie nicht mehr sehr oft.

31.6 Iterables und Array-ähnliche Werte in Arrays umwandeln

Es gibt zwei gängige Methoden, um Iterables und Array-ähnliche Werte in Arrays umzuwandeln:

Ich bevorzuge letzteres – ich finde es selbsterklärender.

31.6.1 Iterables per Spread-Syntax (...) in Arrays umwandeln

Innerhalb eines Array-Literals wandelt Spread-Syntax per ... jedes iterable Objekt in eine Serie von Array-Elementen um. Zum Beispiel:

// Get an Array-like collection from a web browser’s DOM
const domCollection = document.querySelectorAll('a');

// Alas, the collection is missing many Array methods
assert.equal('map' in domCollection, false);

// Solution: convert it to an Array
const arr = [...domCollection];
assert.deepEqual(
  arr.map(x => x.href),
  ['https://2ality.com', 'https://exploringjs.de']);

Die Umwandlung funktioniert, da die DOM-Kollektion iterierbar ist.

31.6.2 Iterables und Array-ähnliche Objekte per Array.from() in Arrays umwandeln

Array.from() kann in zwei Modi verwendet werden.

31.6.2.1 Modus 1 von Array.from(): Umwandlung

Der erste Modus hat die folgende Typsignatur:

.from<T>(iterable: Iterable<T> | ArrayLike<T>): T[]

Die Schnittstelle Iterable ist im Kapitel über synchrone Iteration gezeigt. Die Schnittstelle ArrayLike erschien früher in diesem Kapitel.

Mit einem einzigen Parameter wandelt Array.from() alles, was iterierbar oder Array-ähnlich ist, in ein Array um.

> Array.from(new Set(['a', 'b']))
[ 'a', 'b' ]
> Array.from({length: 2, 0:'a', 1:'b'})
[ 'a', 'b' ]
31.6.2.2 Modus 2 von Array.from(): Umwandlung und Mapping

Der zweite Modus von Array.from() umfasst zwei Parameter:

.from<T, U>(
  iterable: Iterable<T> | ArrayLike<T>,
  mapFunc: (v: T, i: number) => U,
  thisArg?: any)
  : U[]

In diesem Modus macht Array.from() mehrere Dinge:

Anders ausgedrückt: Wir gehen von einem Iterable mit Elementen vom Typ T zu einem Array mit Elementen vom Typ U.

Dies ist ein Beispiel:

> Array.from(new Set(['a', 'b']), x => x + x)
[ 'aa', 'bb' ]

31.7 Arrays mit beliebiger Länge erstellen und füllen

Der beste Weg, ein Array zu erstellen, ist über ein Array-Literal. Wir können es jedoch nicht immer verwenden: Das Array ist möglicherweise zu groß, wir kennen seine Länge während der Entwicklung möglicherweise nicht, oder wir möchten seine Länge flexibel halten. Dann empfehle ich die folgenden Techniken zum Erstellen und möglicherweise Füllen von Arrays.

31.7.1 Brauchen Sie ein leeres Array, das Sie später vollständig füllen?

> new Array(3)
[ , , ,]

Beachten Sie, dass das Ergebnis drei Löcher (leere Slots) hat – das letzte Komma in einem Array-Literal wird immer ignoriert.

31.7.2 Brauchen Sie ein Array, das mit einem primitiven Wert gefüllt ist?

> new Array(3).fill(0)
[0, 0, 0]

Hinweis: Wenn wir .fill() mit einem Objekt verwenden, verweist jedes Array-Element auf dieses Objekt (teilt es).

const arr = new Array(3).fill({});
arr[0].prop = true;
assert.deepEqual(
  arr, [
    {prop: true},
    {prop: true},
    {prop: true},
  ]);

Der nächste Unterabschnitt erklärt, wie dies behoben werden kann.

31.7.3 Brauchen Sie ein Array, das mit Objekten gefüllt ist?

> new Array(3).fill(0)
[0, 0, 0]

Bei großen Größen kann das temporäre Array ziemlich viel Speicher verbrauchen. Der folgende Ansatz hat diesen Nachteil nicht, ist aber weniger selbsterklärend:

> Array.from({length: 3}, () => ({}))
[{}, {}, {}]

Anstelle eines temporären Arrays verwenden wir ein temporäres Array-ähnliches Objekt.

31.7.4 Brauchen Sie eine Zahlenreihe?

function createRange(start, end) {
  return Array.from({length: end-start}, (_, i) => i+start);
}
assert.deepEqual(
  createRange(2, 5),
  [2, 3, 4]);

Hier ist eine alternative, leicht hackige Technik zum Erstellen von Zahlenreihen, die bei Null beginnen:

/** Returns an iterable */
function createRange(end) {
  return new Array(end).keys();
}
assert.deepEqual(
  Array.from(createRange(4)),
  [0, 1, 2, 3]);

Das funktioniert, weil .keys() Löcher wie undefined-Elemente behandelt und ihre Indizes auflistet.

31.7.5 Verwenden Sie ein Typed Array, wenn die Elemente alle ganzen Zahlen oder alle Fließkommazahlen sind

Wenn wir mit Arrays von ganzen Zahlen oder Fließkommazahlen arbeiten, sollten wir Typed Arrays in Betracht ziehen, die für diesen Zweck entwickelt wurden.

31.8 Mehrdimensionale Arrays

JavaScript hat keine echten mehrdimensionalen Arrays; wir müssen auf Arrays zurückgreifen, deren Elemente Arrays sind.

function initMultiArray(...dimensions) {
  function initMultiArrayRec(dimIndex) {
    if (dimIndex >= dimensions.length) {
      return 0;
    } else {
      const dim = dimensions[dimIndex];
      const arr = [];
      for (let i=0; i<dim; i++) {
        arr.push(initMultiArrayRec(dimIndex+1));
      }
      return arr;
    }
  }
  return initMultiArrayRec(0);
}

const arr = initMultiArray(4, 3, 2);
arr[3][2][1] = 'X'; // last in each dimension
assert.deepEqual(arr, [
  [ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
  [ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
  [ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
  [ [ 0, 0 ], [ 0, 0 ], [ 0, 'X' ] ],
]);

31.9 Weitere Array-Features (Fortgeschrittene)

In diesem Abschnitt betrachten wir Phänomene, die uns bei der Arbeit mit Arrays nicht oft begegnen.

31.9.1 Array-Indizes sind (leicht spezielle) Property-Keys

Man könnte denken, dass Array-Elemente etwas Besonderes sind, weil wir sie über Zahlen ansprechen. Aber der Klammeroperator [] dafür ist derselbe Operator, der für den Zugriff auf Properties verwendet wird. Er konvertiert jeden Wert (der kein Symbol ist) in einen String. Daher sind Array-Elemente (fast) normale Properties (Zeile A), und es spielt keine Rolle, ob wir Zahlen oder Strings als Indizes verwenden (Zeilen B und C).

const arr = ['a', 'b'];
arr.prop = 123;
assert.deepEqual(
  Object.keys(arr),
  ['0', '1', 'prop']); // (A)

assert.equal(arr[0], 'a');  // (B)
assert.equal(arr['0'], 'a'); // (C)

Um die Sache noch verwirrender zu machen, ist dies nur, wie die Sprachspezifikation Dinge definiert (die Theorie von JavaScript, wenn Sie so wollen). Die meisten JavaScript-Engines optimieren intern und verwenden tatsächlich ganze Zahlen, um auf Array-Elemente zuzugreifen (die Praxis von JavaScript, wenn Sie so wollen).

Property-Keys (Strings!), die für Array-Elemente verwendet werden, werden als Indizes bezeichnet. Ein String str ist ein Index, wenn seine Umwandlung in eine 32-Bit vorzeichenlose Ganzzahl und zurück zum ursprünglichen Wert führt. Geschrieben als Formel:

ToString(ToUint32(str)) === str
31.9.1.1 Indizes auflisten

Beim Auflisten von Property-Keys werden Indizes speziell behandelt – sie kommen immer zuerst und werden wie Zahlen sortiert ('2' kommt vor '10').

const arr = [];
arr.prop = true;
arr[1] = 'b';
arr[0] = 'a';

assert.deepEqual(
  Object.keys(arr),
  ['0', '1', 'prop']);

Beachten Sie, dass .length, .entries() und .keys() Array-Indizes als Zahlen behandeln und Nicht-Index-Properties ignorieren.

assert.equal(arr.length, 2);
assert.deepEqual(
  Array.from(arr.keys()), [0, 1]);
assert.deepEqual(
  Array.from(arr.entries()), [[0, 'a'], [1, 'b']]);

Wir haben Array.from() verwendet, um die von .keys() und .entries() zurückgegebenen Iterables in Arrays umzuwandeln.

31.9.2 Arrays sind Dictionaries und können Löcher haben

Wir unterscheiden zwei Arten von Arrays in JavaScript:

Arrays können in JavaScript dünn besetzt sein, weil Arrays eigentlich Dictionaries von Indizes zu Werten sind.

  Empfehlung: Löcher vermeiden

Bisher haben wir nur dichte Arrays gesehen, und es wird tatsächlich empfohlen, Löcher zu vermeiden: Sie machen unseren Code komplizierter und werden von Array-Methoden nicht konsistent behandelt. Außerdem optimieren JavaScript-Engines dichte Arrays, was sie schneller macht.

31.9.2.1 Löcher erstellen

Wir können Löcher erstellen, indem wir Indizes beim Zuweisen von Elementen überspringen.

const arr = [];
arr[0] = 'a';
arr[2] = 'c';

assert.deepEqual(Object.keys(arr), ['0', '2']); // (A)

assert.equal(0 in arr, true); // element
assert.equal(1 in arr, false); // hole

In Zeile A verwenden wir Object.keys(), weil arr.keys() Löcher behandelt, als wären sie undefined-Elemente, und sie nicht preisgibt.

Eine weitere Möglichkeit, Löcher zu erstellen, ist das Überspringen von Elementen in Array-Literalen:

const arr = ['a', , 'c'];

assert.deepEqual(Object.keys(arr), ['0', '2']);

Wir können auch Array-Elemente löschen:

const arr = ['a', 'b', 'c'];
assert.deepEqual(Object.keys(arr), ['0', '1', '2']);
delete arr[1];
assert.deepEqual(Object.keys(arr), ['0', '2']);
31.9.2.2 Wie behandeln Array-Operationen Löcher?

Leider gibt es viele verschiedene Arten, wie Array-Operationen Löcher behandeln.

Einige Array-Operationen entfernen Löcher.

> ['a',,'b'].filter(x => true)
[ 'a', 'b' ]

Einige Array-Operationen ignorieren Löcher.

> ['a', ,'a'].every(x => x === 'a')
true

Einige Array-Operationen ignorieren, aber behalten Löcher.

> ['a',,'b'].map(x => 'c')
[ 'c', , 'c' ]

Einige Array-Operationen behandeln Löcher als undefined-Elemente.

> Array.from(['a',,'b'], x => x)
[ 'a', undefined, 'b' ]
> Array.from(['a',,'b'].entries())
[[0, 'a'], [1, undefined], [2, 'b']]

Object.keys() verhält sich anders als .keys() (Strings vs. Zahlen, Löcher haben keine Schlüssel).

> Array.from(['a',,'b'].keys())
[ 0, 1, 2 ]
> Object.keys(['a',,'b'])
[ '0', '2' ]

Es gibt keine Regel, die man sich hier merken müsste. Wenn es jemals darauf ankommt, wie eine Array-Operation Löcher behandelt, ist der beste Ansatz, einen schnellen Test in einer Konsole durchzuführen.

31.10 Elemente hinzufügen und entfernen (destruktiv und nicht-destruktiv)

JavaScript's Array ist ziemlich flexibel und ähnelt eher einer Kombination aus Array, Stack und Queue. Dieser Abschnitt untersucht Möglichkeiten, Array-Elemente hinzuzufügen und zu entfernen. Die meisten Operationen können sowohl destruktiv (das Array modifizieren) als auch nicht-destruktiv (eine modifizierte Kopie erzeugen) durchgeführt werden.

31.10.1 Elemente und Arrays voranstellen

Im folgenden Code stellen wir destruktiv einzelne Elemente voran arr1 und ein Array voran arr2:

const arr1 = ['a', 'b'];
arr1.unshift('x', 'y'); // prepend single elements
assert.deepEqual(arr1, ['x', 'y', 'a', 'b']);

const arr2 = ['a', 'b'];
arr2.unshift(...['x', 'y']); // prepend Array
assert.deepEqual(arr2, ['x', 'y', 'a', 'b']);

Spread-Syntax erlaubt uns, ein Array in arr2 zu "unshiften".

Nicht-destruktives Voranstellen erfolgt per Spread-Elemente:

const arr1 = ['a', 'b'];
assert.deepEqual(
  ['x', 'y', ...arr1], // prepend single elements
  ['x', 'y', 'a', 'b']);
assert.deepEqual(arr1, ['a', 'b']); // unchanged!

const arr2 = ['a', 'b'];
assert.deepEqual(
  [...['x', 'y'], ...arr2], // prepend Array
  ['x', 'y', 'a', 'b']);
assert.deepEqual(arr2, ['a', 'b']); // unchanged!

31.10.2 Elemente und Arrays anhängen

Im folgenden Code hängen wir destruktiv einzelne Elemente an arr1 und ein Array an arr2 an:

const arr1 = ['a', 'b'];
arr1.push('x', 'y'); // append single elements
assert.deepEqual(arr1, ['a', 'b', 'x', 'y']);

const arr2 = ['a', 'b'];
arr2.push(...['x', 'y']); // (A) append Array
assert.deepEqual(arr2, ['a', 'b', 'x', 'y']);

Spread-Syntax (...) erlaubt uns, ein Array in arr2 zu "pushen" (Zeile A).

Nicht-destruktives Anhängen erfolgt per Spread-Elemente:

const arr1 = ['a', 'b'];
assert.deepEqual(
  [...arr1, 'x', 'y'], // append single elements
  ['a', 'b', 'x', 'y']);
assert.deepEqual(arr1, ['a', 'b']); // unchanged!

const arr2 = ['a', 'b'];
assert.deepEqual(
  [...arr2, ...['x', 'y']], // append Array
  ['a', 'b', 'x', 'y']);
assert.deepEqual(arr2, ['a', 'b']); // unchanged!

31.10.3 Elemente entfernen

Dies sind drei destruktive Möglichkeiten, Array-Elemente zu entfernen:

// Destructively remove first element:
const arr1 = ['a', 'b', 'c'];
assert.equal(arr1.shift(), 'a');
assert.deepEqual(arr1, ['b', 'c']);

// Destructively remove last element:
const arr2 = ['a', 'b', 'c'];
assert.equal(arr2.pop(), 'c');
assert.deepEqual(arr2, ['a', 'b']);

// Remove one or more elements anywhere:
const arr3 = ['a', 'b', 'c', 'd'];
assert.deepEqual(arr3.splice(1, 2), ['b', 'c']);
assert.deepEqual(arr3, ['a', 'd']);

.splice() wird im Detail in der Schnellreferenz am Ende dieses Kapitels behandelt.

Destrukturierung per Rest-Element erlaubt uns, nicht-destruktiv Elemente vom Anfang eines Arrays zu entfernen (Destrukturierung wird später behandelt).

const arr1 = ['a', 'b', 'c'];
// Ignore first element, extract remaining elements
const [, ...arr2] = arr1;

assert.deepEqual(arr2, ['b', 'c']);
assert.deepEqual(arr1, ['a', 'b', 'c']); // unchanged!

Leider muss ein Rest-Element am Ende eines Arrays stehen. Daher können wir es nur verwenden, um Suffixe zu extrahieren.

  Übung: Eine Queue per Array implementieren

exercises/arrays/queue_via_array_test.mjs

31.11 Methoden: Iteration und Transformation (.find(), .map(), .filter(), etc.)

In diesem Abschnitt betrachten wir Array-Methoden zum Iterieren über Arrays und zum Transformieren von Arrays.

31.11.1 Callbacks für Iterations- und Transformationsmethoden

Alle Iterations- und Transformationsmethoden verwenden Callbacks. Erstere füttern alle iterierten Werte an ihre Callbacks; letztere fragen ihre Callbacks, wie Arrays zu transformieren sind.

Diese Callbacks haben Typsignaturen, die wie folgt aussehen:

callback: (value: T, index: number, array: Array<T>) => boolean

Das heißt, der Callback erhält drei Parameter (es steht ihm frei, einen davon zu ignorieren):

Was der Callback zurückgeben soll, hängt von der Methode ab, an die er übergeben wird. Möglichkeiten sind:

Beide Methoden werden später genauer beschrieben.

31.11.2 Elemente suchen: .find(), .findIndex()

.find() gibt das erste Element zurück, für das sein Callback einen wahrheitsgemäßen Wert zurückgibt (und undefined, wenn nichts gefunden wird).

> [6, -5, 8].find(x => x < 0)
-5
> [6, 5, 8].find(x => x < 0)
undefined

.findIndex() gibt den Index des ersten Elements zurück, für das sein Callback einen wahrheitsgemäßen Wert zurückgibt (und -1, wenn nichts gefunden wird).

> [6, -5, 8].findIndex(x => x < 0)
1
> [6, 5, 8].findIndex(x => x < 0)
-1

.findIndex() kann wie folgt implementiert werden:

function findIndex(arr, callback) {
  for (const [i, x] of arr.entries()) {
    if (callback(x, i, arr)) {
      return i;
    }
  }
  return -1;
}

31.11.3 .map(): Kopieren und dabei Elemente neue Werte geben

.map() gibt eine modifizierte Kopie des Empfängers zurück. Die Elemente der Kopie sind die Ergebnisse der Anwendung des Callbacks von map auf die Elemente des Empfängers.

All das ist leichter durch Beispiele zu verstehen:

> [1, 2, 3].map(x => x * 3)
[ 3, 6, 9 ]
> ['how', 'are', 'you'].map(str => str.toUpperCase())
[ 'HOW', 'ARE', 'YOU' ]
> [true, true, true].map((_x, index) => index)
[ 0, 1, 2 ]

.map() kann wie folgt implementiert werden:

function map(arr, mapFunc) {
  const result = [];
  for (const [i, x] of arr.entries()) {
    result.push(mapFunc(x, i, arr));
  }
  return result;
}

  Übung: Zeilen nummerieren mit .map()

exercises/arrays/number_lines_test.mjs

31.11.4 .flatMap(): zu null oder mehr Werten mappen

Die Typsignatur von Array<T>.prototype.flatMap() ist:

.flatMap<U>(
  callback: (value: T, index: number, array: T[]) => U|Array<U>,
  thisValue?: any
): U[]

Sowohl .map() als auch .flatMap() nehmen eine Funktion callback als Parameter, die steuert, wie ein Eingabe-Array in ein Ausgabe-Array übersetzt wird.

Dies ist .flatMap() in Aktion:

> ['a', 'b', 'c'].flatMap(x => [x,x])
[ 'a', 'a', 'b', 'b', 'c', 'c' ]
> ['a', 'b', 'c'].flatMap(x => [x])
[ 'a', 'b', 'c' ]
> ['a', 'b', 'c'].flatMap(x => [])
[]

Wir werden Anwendungsfälle als nächstes betrachten, bevor wir erforschen, wie diese Methode implementiert werden könnte.

31.11.4.1 Anwendungsfall: gleichzeitig filtern und mappen

Das Ergebnis der Array-Methode .map() hat immer die gleiche Länge wie das Array, auf dem es aufgerufen wird. Das heißt, sein Callback kann keine Array-Elemente überspringen, an denen es nicht interessiert ist. Die Fähigkeit von .flatMap(), dies zu tun, ist im nächsten Beispiel nützlich.

Wir verwenden die folgende Funktion processArray(), um ein Array zu erstellen, das wir dann per .flatMap() filtern und mappen werden:

function processArray(arr, callback) {
  return arr.map(x => {
    try {
      return { value: callback(x) };
    } catch (e) {
      return { error: e };
    }
  });
}

Als nächstes erstellen wir ein Array results über processArray():

const results = processArray([1, -5, 6], throwIfNegative);
assert.deepEqual(results, [
  { value: 1 },
  { error: new Error('Illegal value: -5') },
  { value: 6 },
]);

function throwIfNegative(value) {
  if (value < 0) {
    throw new Error('Illegal value: '+value);
  }
  return value;
}

Wir können jetzt .flatMap() verwenden, um nur die Werte oder nur die Fehler aus results zu extrahieren:

const values = results.flatMap(
  result => result.value ? [result.value] : []);
assert.deepEqual(values, [1, 6]);
  
const errors = results.flatMap(
  result => result.error ? [result.error] : []);
assert.deepEqual(errors, [new Error('Illegal value: -5')]);
31.11.4.2 Anwendungsfall: einzelne Eingabewerte auf mehrere Ausgabewerte mappen

Die Array-Methode .map() ordnet jedem Eingabe-Array-Element ein Ausgabe-Element zu. Aber was, wenn wir es auf mehrere Ausgabe-Elemente abbilden wollen?

Das wird im folgenden Beispiel notwendig:

> stringsToCodePoints(['many', 'a', 'moon'])
['m', 'a', 'n', 'y', 'a', 'm', 'o', 'o', 'n']

Wir wollen ein Array von Strings in ein Array von Unicode-Zeichen (Codepunkten) umwandeln. Die folgende Funktion erreicht dies per .flatMap():

function stringsToCodePoints(strs) {
  return strs.flatMap(str => Array.from(str));
}
31.11.4.3 Eine einfache Implementierung

Wir können .flatMap() wie folgt implementieren. Hinweis: Diese Implementierung ist einfacher als die eingebaute Version, die z. B. mehr Prüfungen durchführt.

function flatMap(arr, mapFunc) {
  const result = [];
  for (const [index, elem] of arr.entries()) {
    const x = mapFunc(elem, index, arr);
    // We allow mapFunc() to return non-Arrays
    if (Array.isArray(x)) {
      result.push(...x);
    } else {
      result.push(x);
    }
  }
  return result;
}

  Übungen: .flatMap()

31.11.5 .filter(): nur einige der Elemente behalten

Die Array-Methode .filter() gibt ein Array zurück, das alle Elemente sammelt, für die der Callback einen wahrheitsgemäßen Wert zurückgibt.

Zum Beispiel

> [-1, 2, 5, -7, 6].filter(x => x >= 0)
[ 2, 5, 6 ]
> ['a', 'b', 'c', 'd'].filter((_x,i) => (i%2)===0)
[ 'a', 'c' ]

.filter() kann wie folgt implementiert werden:

function filter(arr, filterFunc) {
  const result = [];
  for (const [i, x] of arr.entries()) {
    if (filterFunc(x, i, arr)) {
      result.push(x);
    }
  }
  return result;
}

  Übung: Leere Zeilen mit .filter() entfernen

exercises/arrays/remove_empty_lines_filter_test.mjs

31.11.6 .reduce(): einen Wert aus einem Array ableiten (Fortgeschrittene)

Die Methode .reduce() ist ein mächtiges Werkzeug zur Berechnung einer „Zusammenfassung“ eines Arrays arr. Eine Zusammenfassung kann jede Art von Wert sein:

reduce ist in der funktionalen Programmierung auch als foldl („fold left“) bekannt und dort beliebt. Eine Einschränkung ist, dass es Code schwer verständlich machen kann.

.reduce() hat die folgende Typsignatur (innerhalb eines Array<T>):

.reduce<U>(
  callback: (accumulator: U, element: T, index: number, array: T[]) => U,
  init?: U)
  : U

T ist der Typ der Array-Elemente, U ist der Typ der Zusammenfassung. Die beiden können gleich oder verschieden sein. accumulator ist nur ein anderer Name für „Zusammenfassung“.

Um die Zusammenfassung eines Arrays arr zu berechnen, füttert .reduce() alle Array-Elemente nacheinander an seinen Callback:

const accumulator_0 = callback(init, arr[0]);
const accumulator_1 = callback(accumulator_0, arr[1]);
const accumulator_2 = callback(accumulator_1, arr[2]);
// Etc.

callback kombiniert die zuvor berechnete Zusammenfassung (gespeichert in seinem Parameter accumulator) mit dem aktuellen Array-Element und gibt den nächsten accumulator zurück. Das Ergebnis von .reduce() ist der finale Accumulator – das letzte Ergebnis von callback, nachdem es alle Elemente besucht hat.

Anders ausgedrückt: callback leistet die meiste Arbeit; .reduce() ruft es nur auf nützliche Weise auf.

Man könnte sagen, dass der Callback Array-Elemente in den Accumulator "gefaltet" (folded) werden. Deshalb wird diese Operation in der funktionalen Programmierung "fold" genannt.

31.11.6.1 Ein erstes Beispiel

Betrachten wir ein Beispiel für .reduce() in Aktion: die Funktion addAll() berechnet die Summe aller Zahlen in einem Array arr.

function addAll(arr) {
  const startSum = 0;
  const callback = (sum, element) => sum + element;
  return arr.reduce(callback, startSum);
}
assert.equal(addAll([1,  2, 3]), 6); // (A)
assert.equal(addAll([7, -4, 2]), 5);

In diesem Fall enthält der Accumulator die Summe aller Array-Elemente, die callback bereits besucht hat.

Wie wurde das Ergebnis 6 aus dem Array in Zeile A abgeleitet? Über die folgenden Aufrufe von callback:

callback(0, 1) --> 1
callback(1, 2) --> 3
callback(3, 3) --> 6

Hinweise

Alternativ hätten wir addAll() über eine for-of-Schleife implementieren können:

function addAll(arr) {
  let sum = 0;
  for (const element of arr) {
    sum = sum + element;
  }
  return sum;
}

Es ist schwer zu sagen, welche der beiden Implementierungen "besser" ist: die auf .reduce() basierende ist etwas prägnanter, während die auf for-of basierende möglicherweise etwas leichter zu verstehen ist – besonders wenn jemand mit funktionaler Programmierung nicht vertraut ist.

31.11.6.2 Beispiel: Indizes per .reduce() finden

Die folgende Funktion ist eine Implementierung der Array-Methode .indexOf(). Sie gibt den ersten Index zurück, an dem der gegebene searchValue im Array arr vorkommt:

const NOT_FOUND = -1;
function indexOf(arr, searchValue) {
  return arr.reduce(
    (result, elem, index) => {
      if (result !== NOT_FOUND) {
        // We have already found something: don’t change anything
        return result;
      } else if (elem === searchValue) {
        return index;
      } else {
        return NOT_FOUND;
      }
    },
    NOT_FOUND);
}
assert.equal(indexOf(['a', 'b', 'c'], 'b'), 1);
assert.equal(indexOf(['a', 'b', 'c'], 'x'), -1);

Eine Einschränkung von .reduce() ist, dass wir nicht vorzeitig abbrechen können (in einer for-of-Schleife können wir break verwenden). Hier geben wir das Ergebnis sofort zurück, sobald wir es gefunden haben.

31.11.6.3 Beispiel: Verdoppeln von Array-Elementen

Die Funktion double(arr) gibt eine Kopie von inArr zurück, deren Elemente alle mit 2 multipliziert sind

function double(inArr) {
  return inArr.reduce(
    (outArr, element) => {
      outArr.push(element * 2);
      return outArr;
    },
    []);
}
assert.deepEqual(
  double([1, 2, 3]),
  [2, 4, 6]);

Wir modifizieren den Anfangswert [], indem wir hinein pushen. Eine nicht-destruktive, funktionalere Version von double() sieht wie folgt aus

function double(inArr) {
  return inArr.reduce(
    // Don’t change `outArr`, return a fresh Array
    (outArr, element) => [...outArr, element * 2],
    []);
}
assert.deepEqual(
  double([1, 2, 3]),
  [2, 4, 6]);

Diese Version ist eleganter, aber auch langsamer und verbraucht mehr Speicher.

  Aufgaben: .reduce()

31.12 .sort(): Arrays sortieren

.sort() hat die folgende Typdefinition

sort(compareFunc?: (a: T, b: T) => number): this

Standardmäßig sortiert .sort() Zeichenkettendarstellungen der Elemente. Diese Darstellungen werden über < verglichen. Dieser Operator vergleicht lexikografisch (die ersten Zeichen sind am bedeutsamsten). Dies sehen wir beim Sortieren von Zahlen

> [200, 3, 10].sort()
[ 10, 200, 3 ]

Beim Sortieren von Zeichenketten in natürlicher Sprache müssen wir bedenken, dass sie gemäß ihren Code-Einheiten-Werten (Zeichencodes) verglichen werden

> ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'].sort()
[ 'Cookie', 'Pie', 'cookie', 'pie', 'Éclair', 'éclair' ]

Alle unakzentuierten Großbuchstaben kommen vor allen unakzentuierten Kleinbuchstaben, die wiederum vor allen akzentuierten Buchstaben kommen. Wir können Intl, die JavaScript Internationalization API verwenden, wenn wir eine korrekte Sortierung für natürliche Sprachen wünschen.

.sort() sortiert in place; es ändert und gibt seinen Empfänger zurück

> const arr = ['a', 'c', 'b'];
> arr.sort() === arr
true
> arr
[ 'a', 'b', 'c' ]

31.12.1 Sortierreihenfolge anpassen

Wir können die Sortierreihenfolge über den Parameter compareFunc anpassen, der eine Zahl zurückgeben muss, die

  Tipp zum Merken dieser Regeln

Eine negative Zahl ist kleiner als null (usw.).

31.12.2 Zahlen sortieren

Wir können diese Hilfsfunktion verwenden, um Zahlen zu sortieren

function compareNumbers(a, b) {
  if (a < b) {
    return -1;
  } else if (a === b) {
    return 0;
  } else {
    return 1;
  }
}
assert.deepEqual(
  [200, 3, 10].sort(compareNumbers),
  [3, 10, 200]);

Das Folgende ist eine schnelle und schmutzige Alternative.

> [200, 3, 10].sort((a,b) => a - b)
[ 3, 10, 200 ]

Die Nachteile dieses Ansatzes sind

31.12.3 Objekte sortieren

Wir müssen auch eine Vergleichsfunktion verwenden, wenn wir Objekte sortieren wollen. Als Beispiel zeigt der folgende Code, wie Objekte nach Alter sortiert werden.

const arr = [ {age: 200}, {age: 3}, {age: 10} ];
assert.deepEqual(
  arr.sort((obj1, obj2) => obj1.age - obj2.age),
  [{ age: 3 }, { age: 10 }, { age: 200 }] );

  Aufgabe: Objekte nach Namen sortieren

exercises/arrays/sort_objects_test.mjs

31.13 Schnellreferenz: Array

Legende

31.13.1 new Array()

new Array(n) erstellt ein Array der Länge n, das n Lücken enthält

// Trailing commas are always ignored.
// Therefore: number of commas = number of holes
assert.deepEqual(new Array(3), [,,,]);

new Array() erstellt ein leeres Array. Ich empfehle jedoch, stattdessen immer [] zu verwenden.

31.13.2 Statische Methoden von Array

31.13.3 Methoden von Array.prototype

31.13.4 Quellen

  Quiz

Siehe Quiz-App.