Kapitel 18. Arrays
Inhaltsverzeichnis
Das Buch kaufen
(Werbung, bitte nicht blockieren.)

Kapitel 18. Arrays

Ein Array ist eine Abbildung von Indizes (natürliche Zahlen, beginnend bei Null) auf beliebige Werte. Die Werte (der Wertebereich der Abbildung) werden als Elemente des Arrays bezeichnet. Die bequemste Art, ein Array zu erstellen, ist über ein Array-Literal. Ein solches Literal zählt die Array-Elemente auf; die Position eines Elements legt implizit seinen Index fest.

In diesem Kapitel werde ich zuerst die grundlegenden Array-Mechanismen wie indizierten Zugriff und die length-Eigenschaft behandeln, und dann die Array-Methoden durchgehen.

Übersicht

Dieser Abschnitt bietet einen schnellen Überblick über Arrays. Details werden später erklärt.

Als erstes Beispiel erstellen wir ein Array arr über ein Array-Literal (siehe Erstellen von Arrays) und greifen auf Elemente zu (siehe Array-Indizes)

> var arr = [ 'a', 'b', 'c' ]; // array literal
> arr[0]  // get element 0
'a'
> arr[0] = 'x';  // set element 0
> arr
[ 'x', 'b', 'c' ]

Wir können die Array-Eigenschaft length (siehe length) verwenden, um Elemente zu entfernen und anzuhängen:

> var arr = [ 'a', 'b', 'c' ];
> arr.length
3
> arr.length = 2;  // remove an element
> arr
[ 'a', 'b' ]
> arr[arr.length] = 'd';  // append an element
> arr
[ 'a', 'b', 'd' ]

Die Array-Methode push() bietet eine weitere Möglichkeit, ein Element anzuhängen.

> var arr = [ 'a', 'b' ];
> arr.push('d')
3
> arr
[ 'a', 'b', 'd' ]

Arrays sind Abbildungen, keine Tupel

Der ECMAScript-Standard definiert Arrays als Abbildungen (Dictionaries) von Indizes auf Werte. Mit anderen Worten, Arrays müssen nicht zusammenhängend sein und können Löcher aufweisen. Zum Beispiel:

> var arr = [];
> arr[0] = 'a';
'a'
> arr[2] = 'b';
'b'
> arr
[ 'a', , 'b' ]

Das vorherige Array hat ein Loch: Es gibt kein Element am Index 1. Löcher in Arrays erklärt Löcher im Detail.

Beachten Sie, dass die meisten JavaScript-Engines Arrays ohne Löcher intern optimieren und sie zusammenhängend speichern.

Sie erstellen ein Array über ein Array-Literal.

var myArray = [ 'a', 'b', 'c' ];

Nachgestellte Kommas in Arrays werden ignoriert.

> [ 'a', 'b' ].length
2
> [ 'a', 'b', ].length
2
> [ 'a', 'b', ,].length  // hole + trailing comma
3

Wenn Sie mehrere Dimensionen für Elemente benötigen, müssen Sie Arrays verschachteln. Wenn Sie solche verschachtelten Arrays erstellen, können die innersten Arrays nach Bedarf wachsen. Wenn Sie jedoch direkten Zugriff auf Elemente wünschen, müssen Sie mindestens die äußeren Arrays erstellen. Im folgenden Beispiel erstelle ich eine Drei-mal-Drei-Matrix für Tic-tac-toe. Die Matrix ist vollständig mit Daten gefüllt (im Gegensatz zum Wachsenlassen von Zeilen nach Bedarf):

// Create the Tic-tac-toe board
var rows = [];
for (var rowCount=0; rowCount < 3; rowCount++) {
    rows[rowCount] = [];
    for (var colCount=0; colCount < 3; colCount++) {
        rows[rowCount][colCount] = '.';
    }
}

// Set an X in the upper right corner
rows[0][2] = 'X';  // [row][column]

// Print the board
rows.forEach(function (row) {
    console.log(row.join(' '));
});

Hier ist die Ausgabe.

. . X
. . .
. . .

Ich wollte, dass das Beispiel den allgemeinen Fall demonstriert. Offensichtlich können Sie, wenn eine Matrix so klein und mit festen Dimensionen ist, sie über ein Array-Literal einrichten.

var rows = [ ['.','.','.'], ['.','.','.'], ['.','.','.'] ];

Array-Indizes

Wenn Sie mit Array-Indizes arbeiten, müssen Sie die folgenden Grenzen beachten:

  • Indizes sind Zahlen i im Bereich 0 ≤ i < 232−1.
  • Die maximale Länge beträgt 232−1.

Indizes außerhalb des Bereichs werden als normale Eigenschaftsschlüssel (Zeichenketten!) behandelt. Sie erscheinen nicht als Array-Elemente und beeinflussen die length-Eigenschaft nicht. Zum Beispiel:

> var arr = [];

> arr[-1] = 'a';
> arr
[]
> arr['-1']
'a'

> arr[4294967296] = 'b';
> arr
[]
> arr['4294967296']
'b'

Zusätzlich zum Löschen von Eigenschaften löscht der delete-Operator auch Array-Elemente. Das Löschen von Elementen erzeugt Löcher (die length-Eigenschaft wird nicht aktualisiert):

> var arr = [ 'a', 'b' ];
> arr.length
2
> delete arr[1]  // does not update length
true
> arr
[ 'a',  ]
> arr.length
2

Sie können auch nachgestellte Array-Elemente löschen, indem Sie die Länge eines Arrays verringern (siehe length für Details). Um Elemente zu entfernen, ohne Löcher zu erzeugen (d. h. die Indizes nachfolgender Elemente werden dekrementiert), verwenden Sie Array.prototype.splice() (siehe Hinzufügen und Entfernen von Elementen (destruktiv)). In diesem Beispiel entfernen wir zwei Elemente am Index 1.

> var arr = ['a', 'b', 'c', 'd'];
> arr.splice(1, 2) // returns what has been removed
[ 'b', 'c' ]
> arr
[ 'a', 'd' ]

Array-Indizes im Detail

Tipp

Dies ist ein fortgeschrittener Abschnitt. Normalerweise müssen Sie die hier erklärten Details nicht kennen.

Array-Indizes sind nicht das, was sie scheinen. Bisher habe ich so getan, als wären Array-Indizes Zahlen. Und so implementieren JavaScript-Engines Arrays intern. Die ECMAScript-Spezifikation sieht Indizes jedoch anders. Paraphrasierung von Abschnitt 15.4

  • Ein Eigenschaftsschlüssel P (eine Zeichenkette) ist einArray-Index, wenn und nur wenn ToString(ToUint32(P)) gleich P ist und ToUint32(P) nicht gleich 232−1 ist. Was das bedeutet, wird gleich erklärt.
  • Eine Array-Eigenschaft, deren Schlüssel ein Array-Index ist, wird alsElement bezeichnet.

Mit anderen Worten, in der Welt der Spezifikation werden alle Werte in Klammern in Zeichenketten umgewandelt und als Eigenschaftsschlüssel interpretiert, selbst Zahlen. Die folgende Interaktion zeigt dies:

> var arr = ['a', 'b'];
> arr['0']
'a'
> arr[0]
'a'

Um ein Array-Index zu sein, muss ein Eigenschaftsschlüssel P (eine Zeichenkette!) gleich dem Ergebnis der folgenden Berechnung sein:

  1. Wandeln Sie P in eine Zahl um.
  2. Wandeln Sie die Zahl in einen 32-Bit vorzeichenlosen Integer um.
  3. Wandeln Sie den Integer in eine Zeichenkette um.

Das bedeutet, dass ein Array-Index eine stringifizierte Ganzzahl i im 32-Bit-Bereich 0 ≤ i < 232−1 sein muss. Die Obergrenze wurde in der Spezifikation explizit ausgeschlossen (wie zuvor zitiert). Sie ist für die maximale Länge reserviert. Um zu sehen, wie diese Definition funktioniert, verwenden wir dieFunktion ToUint32() aus 32-Bit-Ganzzahlen über bitweise Operatoren.

Erstens wird eine Zeichenkette, die keine Zahl enthält, immer in 0 umgewandelt, was nach der Stringifizierung nicht mit der Zeichenkette übereinstimmt.

> ToUint32('xyz')
0
> ToUint32('?@#!')
0

Zweitens wird eine stringifizierte Ganzzahl außerhalb des Bereichs ebenfalls in eine völlig andere Ganzzahl umgewandelt, die nach der Stringifizierung nicht mit der Zeichenkette übereinstimmt.

> ToUint32('-1')
4294967295
> Math.pow(2, 32)
4294967296
> ToUint32('4294967296')
0

Drittens werden stringifizierte Nicht-Ganzzahlen in Ganzzahlen umgewandelt, die wiederum unterschiedlich sind.

> ToUint32('1.371')
1

Beachten Sie, dass die Spezifikation auch sicherstellt, dass Array-Indizes keine Exponenten haben.

> ToUint32('1e3')
1000

Und dass sie keine führenden Nullen haben.

> var arr = ['a', 'b'];
> arr['0']  // array index
'a'
> arr['00'] // normal property
undefined

length

Die grundlegende Funktion der length-Eigenschaft besteht darin, den höchsten Index in einem Array zu verfolgen:

> [ 'a', 'b' ].length
2
> [ 'a', , 'b' ].length
3

Somit zählt length nicht die Anzahl der Elemente, sodass Sie Ihre eigene Funktion dafür schreiben müssten. Zum Beispiel:

function countElements(arr) {
    var elemCount = 0;
    arr.forEach(function () {
        elemCount++;
    });
    return elemCount;
}

Um Elemente (nicht-Löcher) zu zählen, haben wir die Tatsache genutzt, dass forEach Löcher überspringt. Hier ist die Interaktion:

> countElements([ 'a', 'b' ])
2
> countElements([ 'a', , 'b' ])
2

Löcher in Arrays

Arrays sind Abbildungen von Indizes auf Werte. Das bedeutet, dass ArraysLöcher haben können, Indizes kleiner als die Länge, die im Array fehlen. Das Lesen eines Elements an einem dieser Indizes gibt undefined zurück.

Tipp

Es wird empfohlen, Löcher in Arrays zu vermeiden. JavaScript behandelt sie inkonsistent (d. h. einige Methoden ignorieren sie, andere nicht). Glücklicherweise müssen Sie normalerweise nicht wissen, wie Löcher behandelt werden: Sie sind selten nützlich und beeinträchtigen die Leistung negativ.

Einige Operationen, die Arrays betreffen, ignorieren Löcher, während andere sie berücksichtigen. Dieser Abschnitt erklärt die Details.

apply() wandelt jedes Loch in ein Argument um, dessen Wert undefined ist. Die folgende Interaktion zeigt dies: Die Funktion f() gibt ihre Argumente als Array zurück. Wenn wir apply() ein Array mit drei Löchern übergeben, um f() aufzurufen, erhält letzteres drei undefined-Argumente.

> function f() { return [].slice.call(arguments) }
> f.apply(null, [ , , ,])
[ undefined, undefined, undefined ]

Das bedeutet, dass wir apply() verwenden können, um ein Array mit undefineds zu erstellen:

> Array.apply(null, Array(3))
[ undefined, undefined, undefined ]

Warnung

apply() übersetzt Löcher in undefineds in leeren Arrays, aber es kann nicht verwendet werden, um Löcher in beliebigen Arrays zu füllen (die Löcher enthalten können oder nicht). Nehmen Sie zum Beispiel das beliebige Array [2].

> Array.apply(null, [2])
[ , ,]

Das Array enthält keine Löcher, daher sollte apply() dasselbe Array zurückgeben. Stattdessen gibt es ein leeres Array der Länge 2 zurück (es enthält nur zwei Löcher). Das liegt daran, dass Array() einzelne Zahlen als Array-Längen interpretiert, nicht als Array-Elemente.

Hinzufügen und Entfernen von Elementen (destruktiv)

Alle Methoden in diesem Abschnitt sind destruktiv:

Array.prototype.shift()

Entfernt das Element am Index 0 und gibt es zurück. Die Indizes nachfolgender Elemente werden um 1 dekrementiert:

> var arr = [ 'a', 'b' ];
> arr.shift()
'a'
> arr
[ 'b' ]
Array.prototype.unshift(elem1?, elem2?, ...)

Stellt die gegebenen Elemente dem Array voran. Sie gibt die neue Länge zurück:

> var arr = [ 'c', 'd' ];
> arr.unshift('a', 'b')
4
> arr
[ 'a', 'b', 'c', 'd' ]
Array.prototype.pop()

Entfernt das letzte Element des Arrays und gibt es zurück:

> var arr = [ 'a', 'b' ];
> arr.pop()
'b'
> arr
[ 'a' ]
Array.prototype.push(elem1?, elem2?, ...)

Fügt die gegebenen Elemente am Ende des Arrays hinzu. Sie gibt die neue Länge zurück:

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

apply() (siehe Function.prototype.apply(thisValue, argArray)) ermöglicht es Ihnen, ein Array arr2 destruktiv an ein anderes Array arr1 anzuhängen:

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

> Array.prototype.push.apply(arr1, arr2)
4
> arr1
[ 'a', 'b', 'c', 'd' ]
Array.prototype.splice(start, deleteCount?, elem1?, elem2?, ...)

Beginnend bei start, entfernt deleteCount Elemente und fügt die gegebenen Elemente ein. Mit anderen Worten, Sie ersetzen die deleteCount Elemente an Position start durch elem1, elem2 usw. Die Methode gibt die entfernten Elemente zurück:

> var arr = [ 'a', 'b', 'c', 'd' ];
> arr.splice(1, 2, 'X');
[ 'b', 'c' ]
> arr
[ 'a', 'X', 'd' ]

Spezielle Parameterwerte

In diesem Beispiel entfernen wir alle Elemente nach und einschließlich des vorletzten Index.

> var arr = [ 'a', 'b', 'c', 'd' ];
> arr.splice(-2)
[ 'c', 'd' ]
> arr
[ 'a', 'b' ]

Diese Methoden sind ebenfalls destruktiv.

Für Zeichenketten können Sie String.prototype.localeCompare verwenden (siehe Zeichenketten vergleichen).

> ['c', 'a', 'b'].sort(function (a,b) { return a.localeCompare(b) })
[ 'a', 'b', 'c' ]

Die folgenden Methoden führen verschiedene nicht-destruktive Operationen auf Arrays aus:

Array.prototype.concat(arr1?, arr2?, ...)

Erstellt ein neues Array, das alle Elemente des Empfängers enthält, gefolgt von allen Elementen des Arrays arr1 und so weiter. Wenn einer der Parameter kein Array ist, wird er als Element zum Ergebnis hinzugefügt (zum Beispiel das erste Argument, 'c', hier):

> var arr = [ 'a', 'b' ];
> arr.concat('c', ['d', 'e'])
[ 'a', 'b', 'c', 'd', 'e' ]

Das Array, auf dem concat() aufgerufen wird, wird nicht geändert.

> arr
[ 'a', 'b' ]
Array.prototype.slice(begin?, end?)

Kopiert Array-Elemente in ein neues Array, beginnend bei begin, bis zum und ausschließlich des Elements bei end:

> [ 'a', 'b', 'c', 'd' ].slice(1, 3)
[ 'b', 'c' ]

Wenn end fehlt, wird die Array-Länge verwendet.

> [ 'a', 'b', 'c', 'd' ].slice(1)
[ 'b', 'c', 'd' ]

Wenn beide Indizes fehlen, wird das Array kopiert.

> [ 'a', 'b', 'c', 'd' ].slice()
[ 'a', 'b', 'c', 'd' ]

Wenn einer der Indizes negativ ist, wird die Array-Länge dazu addiert. Somit bezieht sich -1 auf das letzte Element und so weiter.

> [ 'a', 'b', 'c', 'd' ].slice(1, -1)
[ 'b', 'c' ]
> [ 'a', 'b', 'c', 'd' ].slice(-2)
[ 'c', 'd' ]
Array.prototype.join(separator?)

Erstellt eine Zeichenkette, indem toString() auf alle Array-Elemente angewendet und die Zeichenkette separator zwischen die Ergebnisse gesetzt wird. Wenn separator weggelassen wird, wird ',' verwendet:

> [3, 4, 5].join('-')
'3-4-5'
> [3, 4, 5].join()
'3,4,5'
> [3, 4, 5].join('')
'345'

join() konvertiert undefined und null in leere Zeichenketten.

> [undefined, null].join('#')
'#'

Löcher in Arrays werden ebenfalls in leere Zeichenketten konvertiert.

> ['a',, 'b'].join('-')
'a--b'

Iteration (nicht-destruktiv)

Iterations-Methoden verwenden eine Funktion, um über ein Array zu iterieren. Ich unterscheide drei Arten von Iterationsmethoden, die alle nicht-destruktiv sind: Prüfungsmethoden beobachten hauptsächlich den Inhalt eines Arrays; Transformationsmethoden leiten ein neues Array aus dem Empfänger ab; und Reduktionsmethoden berechnen ein Ergebnis basierend auf den Elementen des Empfängers.

Prüfungsmethoden

Jede Methode, die in diesem Abschnitt beschrieben wird, sieht so aus:

arr.examinationMethod(callback, thisValue?)

Eine solche Methode nimmt die folgenden Parameter an:

Und nun zu den Prüfungsmethoden, deren Signaturen ich gerade beschrieben habe:

Array.prototype.forEach(callback, thisValue?)

Iteriert über die Elemente eines Arrays.

var arr = [ 'apple', 'pear', 'orange' ];
arr.forEach(function (elem) {
    console.log(elem);
});
Array.prototype.every(callback, thisValue?)

Gibt true zurück, wenn der Callback für jedes Element true zurückgibt. Die Iteration stoppt, sobald der Callback false zurückgibt. Beachten Sie, dass das Nicht-Zurückgeben eines Wertes zu einem impliziten Rückgabewert von undefined führt, den every() als false interpretiert. every() funktioniert wie der Allquantor („für alle“).

Dieses Beispiel prüft, ob jede Zahl im Array gerade ist.

> function isEven(x) { return x % 2 === 0 }
> [ 2, 4, 6 ].every(isEven)
true
> [ 2, 3, 4 ].every(isEven)
false

Wenn das Array leer ist, ist das Ergebnis true (und callback wird nicht aufgerufen).

> [].every(function () { throw new Error() })
true
Array.prototype.some(callback, thisValue?)

Gibt true zurück, wenn der Callback für mindestens ein Element true zurückgibt. Die Iteration stoppt, sobald der Callback true zurückgibt. Beachten Sie, dass das Nicht-Zurückgeben eines Wertes zu einem impliziten Rückgabewert von undefined führt, den some als false interpretiert. some() funktioniert wie der Existenzquantor („es existiert“).

Dieses Beispiel prüft, ob eine gerade Zahl im Array vorhanden ist.

> function isEven(x) { return x % 2 === 0 }
> [ 1, 3, 5 ].some(isEven)
false
> [ 1, 2, 3 ].some(isEven)
true

Wenn das Array leer ist, ist das Ergebnis false (und callback wird nicht aufgerufen).

> [].some(function () { throw new Error() })
false

Ein potenzieller Fallstrick von forEach() ist, dass es kein break oder etwas Ähnliches zur vorzeitigen Beendigung der Schleife unterstützt. Wenn Sie dies tun müssen, können Sie some() verwenden.

function breakAtEmptyString(strArr) {
    strArr.some(function (elem) {
        if (elem.length === 0) {
            return true; // break
        }
        console.log(elem);
        // implicit: return undefined (interpreted as false)
    });
}

some() gibt true zurück, wenn ein Abbruch stattgefunden hat, und false andernfalls. Dies ermöglicht es Ihnen, unterschiedlich zu reagieren, je nachdem, ob die Iteration erfolgreich abgeschlossen wurde (was mit for-Schleifen etwas knifflig ist).

Für die Reduktion hat der Callback eine andere Signatur:

function callback(previousValue, currentElement, currentIndex, array)

Der Parameter previousValue ist der von der Callback zuvor zurückgegebene Wert. Wenn die Callback zum ersten Mal aufgerufen wird, gibt es zwei Möglichkeiten (die Beschreibungen gelten für Array.prototype.reduce(); Unterschiede zu reduceRight() werden in Klammern erwähnt):

Es gibt zwei Reduktionsmethoden.

Eine andere Möglichkeit, die reduce-Methode zu betrachten, ist, dass sie einen n-stelligen Operator OP implementiert.

OP1≤i≤n xi

durch eine Reihe von Anwendungen eines binären Operators op2

(...(x1 op2 x2) op2 ...) op2 xn

Das ist, was im vorherigen Codebeispiel passiert ist: Wir haben einen n-stelligen Summenoperator für Arrays über den binären Plusoperator von JavaScript implementiert.

Als Beispiel untersuchen wir die beiden Iterationsrichtungen anhand der folgenden Funktion.

function printArgs(prev, cur, i) {
    console.log('prev:'+prev+', cur:'+cur+', i:'+i);
    return prev + cur;
}

Wie erwartet, iteriert reduce() von links nach rechts.

> ['a', 'b', 'c'].reduce(printArgs)
prev:a, cur:b, i:1
prev:ab, cur:c, i:2
'abc'
> ['a', 'b', 'c'].reduce(printArgs, 'x')
prev:x, cur:a, i:0
prev:xa, cur:b, i:1
prev:xab, cur:c, i:2
'xabc'

Und reduceRight() iteriert von rechts nach links.

> ['a', 'b', 'c'].reduceRight(printArgs)
prev:c, cur:b, i:1
prev:cb, cur:a, i:0
'cba'
> ['a', 'b', 'c'].reduceRight(printArgs, 'x')
prev:x, cur:c, i:2
prev:xc, cur:b, i:1
prev:xcb, cur:a, i:0
'xcba'

Bewährte Praktiken: Arrays durchlaufen

Um ein Array arr zu durchlaufen, haben Sie zwei Möglichkeiten:

Verwenden Sie nicht die for-in-Schleife (siehe for-in), um Arrays zu durchlaufen. Sie iteriert über Indizes, nicht über Werte. Und sie schließt die Schlüssel normaler Eigenschaften ein, einschließlich vererbter.

Weiter: 19. Reguläre Ausdrücke