Es heißt, JavaScript-Entwickler hätten tausend Begriffe für „undefined is not a function“, doch was es in JS wirklich tausendfach gibt, sind Wege zur Array-Initialisierung. Manche dieser Wege sind sinnvoll, manche sind weniger sinnvoll und die meisten Wege bieten interessante Tradeoffs. Das gilt selbst für die vermeintlich einfachste Array-Erstellungs-Variante, das Literal:

let myArray = [];

Das erscheint für das ungeübte Auge wie eine okaye Möglichkeit, ein Array der Länge 0 zu erschaffen, aber dieses Pattern ist nicht immer das Optimalste! Denn zumindest clientseitiges JavaScript will minifiziert werden und ein leeres Array-Literal wird nie kürzer werden als zwei Zeichen (Klammer auf, Klammer zu). Projekte, die Wert darauf legen, jedes Bit einzusparen, definieren deshalb gerne in einem Extra-Modul eine Konstante für das leere Array:

// https://github.com/preactjs/preact/blob/master/src/constants.js
export const EMPTY_ARR = [];

Die Idee dahinter: aus EMPTY_ARR wird im Rahmen der Minifizierung (hoffentlich) eine Ein-Buchstaben-Variable wie x, die bei genauer Betrachtung aus weniger Zeichen besteht als ein handgeschriebenes Array-Literal!

// Vor Minifizierung und Bundleing
import { EMPTY_ARR } from "./constants";
doStuff(EMPTY_ARR);

// Nach Minifizierung und Bundleing
const b=[];a(b)

Werden leere Arrays oft genug verwendet, amortisieren sich irgendwann die Extra-Bytes für die Deklaration der Leeres-Array-Variable. Wir sollten nur aufpassen, dieses Array nicht versehentlich mit Inhalt zu versehen oder auf andere Weise zu modifizieren, denn das resultierende Massaker zu debuggen dürfte interessant werden.

Wenn wir dieses Risiko nicht eingehen, aber dafür unseren inneren OOP-Tiger erwecken wollen, können wir natürlich auch mit new Array() arbeiten:

let myAbstractArrayFactorySingleton = new Array();
myAbstractArrayFactorySingleton.push("ok boomer");

Diese Variante ist eigentlich nur zu empfehlen, wenn wir statt nach Arbeitsstunden oder sonstigen Phantasiemetriken nach produzierten Code-Kilobytes bezahlt werden. In einem solchen Arrangement haben wir nicht nur die Chance, uns als Held der Arbeit zu profilieren, nein; der Array-Constructor bietet darüber hinaus als Extra-Feature an, das Array mit einer vorgegebenen Länge zu initialisieren:

let myArray = new Array(5);
// Array mit length = 5

Spannend hierbei ist, dass das resultierende Array ein Array mit einer length von 5 ist, aber, es aber mitnichten 5 Felder hat! Historisch waren JavaScript-Arrays in erster Linie Lügengebäude und erst im Nebenjob Datenstrukturen. Sie waren ganz normale JavaScript-Objekte, die ein paar Extra-Regeln folgten:

  1. Keys sind numerische Strings
  2. Es existiert ein Extra-Key namens length, dessen Wert der Key mit dem höchsten numerischen Wert + 1 ist

Und wenn wir ehrlich sind, was braucht der Mensch bzw. das Array mehr als das? Wenn wir ein Fake-Array aus einem Objekt und den obigen Regeln bauen, können wir das Lügengebäude problemlos in z.B. For-Schleifen nutzen:

let fake = {
  "0": 23,
  "1": 42,
  "length": 2,
};

for (let i = 0; i < fake.length; i++) {
  console.log(fake[i]); // läuft!
}

Einer der zahlreichen Clous an new Array(x) ist, dass es ein Array anlegt, das zwar eine length von x hat, aber nicht die entsprechenden Felder. Die length an sich ist eine Lüge:

let a = [1, 2, 3]; // Array mit drei Mal echtem Inhalt
console.log(Object.keys(a), a.length) // ["0", "1", "2"], 3

let b = new Array(3); // Array mit Lügen-Length
console.log(Object.keys(b), b.length) // [], 3

Deshalb steht, wenn wir new Array(3) in die JS-Konsole von z.B. Chrome werfen, im Output [empty × 3], was bemerkenswert ist – immerhin ist „empty“ in JavaScript weder Typ noch Wert noch Konzept. Aber anders weiß der Browser ein Flunker-Array nicht zu umschreiben. Ganz allgemein können wir über length sagen, dass es meist irgendwie mit dem Array-Inhalt korreliert, aber etwas Sicheres sagt es nicht aus:

let a = [];
// a.length = 0
// Browser-Repräsentation von a ist []

a[7] = 42;
// a.length = 8
// Browser-Repräsentation von a ist [empty × 7, 42]

a.length = 10;
// a.length = 10
// Browser-Repräsentation von a ist [empty × 7, 42, empty × 2]

Die Clowns unter euch fragen sich vielleicht bereits, was passiert, wenn wir new Array() mit einer Kommazahl füttern:

new Array(23.42); // Höhö

Das Ergebnis: ein Uncaught RangeError mit Verweis auf eine angebliche „Invalid Array Length“. Anscheinend mag der Array-Constructor nur ganzzahlige Inputs haben. Aber das können wir in modernem JavaScript problemlos sicherstellen, denn für ganzzahlige Werte gibt es ja seit einiger Zeit den BigInt-Typ. Damit kann nichts mehr schiefgehen, richtig?

new Array(23n); // Garantiert ganzzahlig!

Das Ergebnis: ein Array der Länge 1 mit 23n als Inhalt. Es stellt sich nämlich raus, dass new Array() als Length-Input zwar unbedingt eine ganze Zahl haben möchte, die aber auch nicht zu ganzzahlig (also ein BigInt) sein darf. Genau genommen ist das Regelwerk von new Array() wie folgt:

  1. Wenn es nur einen Input gibt und dieser eine ganzzahlige Nicht-BigInt-Zahl ist, wird ein Array mit entsprechender Länge, aber ohne Felder oder Inhalt erzeugt
  2. Bei zwei oder mehr Inputs oder bei einem Input, der weder eine ganzzahlige Nicht-BigInt-Zahl noch eine nicht-ganzzahlige Nicht-BigInt-Zahl ist, wird ein Array mit dem Input als Inhalt (und passender Länge) erzeugt.
  3. Bei einem Input, der eine nicht-ganzzahlige Nicht-BigInt-Zahl ist, setzt es einen ReferenceError.

Alles klar? Schreibt mir auf Twitter, wie viele der folgenden Ergebnisse ihr korrekt vorhergesagt habt:

let a = new Array(4e2);
let b = new Array(NaN);
let c = new Array(new Number(42));
let d = new Array(...[5]);
let e = new Array(...new Array(...new Array(5)));
let f = new Array(...new Array(new Number(42n)));

Dieses Verhalten ist nicht nur absolut trivial und für jeden JS-Nerd komplett offensichtlich, sondern auch von TypeScript-Typechecker abgesegnet und daher über jede Kritik erhaben. Doch aus nicht nachvollziehbaren Gründen beschloss die ECMAScript-Arbeitsgruppe, diese wunderschöne API um eine abgespeckte Variante zu ergänzen, die nur einen Teil der Aufgaben von new Array() zu erfüllen vermag. Array.of() ist wie new Array(), nur langweiliger:

let a = Array.of(3);             // [3]
let b = Array.of(3, 5);          // [3, 5]
let b = Array.of(NaN, Infinity); // [NaN, Infinity]
let c = Array.of(42n);           // [42n]

Da könnten wir auch gleich Array-Literale schreiben (wenngleich das, wie schon besprochen, den Eindruck erwecken könnte, dass uns am Ende die Bundle-Größe nicht wichtig sei). Einen echten ECMAScript2077-Ersatz für new Array() mit einem numerischen ganzzahligen Nicht-BigInt-Input gibt es nicht direkt, aber per Manipulation der Array-Length können wir den gleichen Effekt erzeugen:

let a = []
a.length = 7;

// Gleicher Effekt:
let b = new Array(7);

Obwohl es bei a wie b um die Manipulation der length geht, bietet die a-Variante mehr Freiheiten, denn hier herrschen die für JavaScriptler (und nur für JavaScriptler) etwas weniger überraschenden Typ-Konvertier-Regeln:

let a = []

a.length = 3;
// Klappt, length ist 3

a.length = "5";
// Klappt, length ist 5

a.length = new Number(7);
// Klappt, length ist 7

a.length = null;
// Klappt, length ist 0

a.length = { toString: () => "2e4" };
// Klappt, length ist 20000

a.length = 23n
// Uncaught TypeError: Cannot convert a BigInt value to a number

Es erscheint vielleicht etwas seltsam, dass null akzeptiert und zu 0 verwandelt wird, während 23n zum Fehler führt, aber BigInt ist tatsächlich in der Lage, Zahlen zu repräsentieren, die nicht als JS-Number abbildbar sind. Die Umwandlung von null oder "5" in Zahlen ist zwar auch ein bisschen seltsam, aber zumindest innerhalb von JavaScript etablierte Praxis und insofern etwas weniger wirr, als das Regelwerk rund um new Array().

Ganz am Rande: der einfachste Weg, ein Array zu leeren, besteht darin, length auf 0 zu setzen, wobei natürlich auch jede der folgenden Alternativ-Varianten zum Erfolg führt:

let a = [ 1, 2, 3]

// In langweilig:
a.length = 0;

// Viel besser:
a.length = "";
a.length = null;
a.length = "00000000000000";
a.length = { toString: () => 0 }

Aber ganz gleich, wie wir zu einem Array mit Inhalt/Length-Diskrepanz kommen: wie können wir diesem Array zu Inhalt verhelfen? Eine Möglichkeit ist die fill()-Methode:

new Array(7).fill(42, 0, 7)
// > [42, 42, 42, 42, 42, 42, 42]

Wichtig ist hierbei, dass fill(), das in diesem Beispiel an allen Indizes von 0 bis 7 den Wert 42 einsetzt, sich nur an der length seines Ziels orientiert. Würden wir new Array() mit 4 füttern, würden auch nur die Felder 0 bis 3 mit 42 befüllt, denn die Felder 4 bis 6 existieren zwar ebenso wenig wie wie 0 bis 3 (wir erinnern uns), aber die Existenz von 0 bis 3 wird wenigstens von der length behauptet, und das ist alles was fill() braucht:

// Keine Felder, length = 7, 0-7 befüllt
new Array(7).fill(42, 0, 7); // [ 7 × 42 ]

// Keine Felder, length = 7, 0-7 befüllt
new Array(4).fill(42, 0, 7); // nur [ 4 × 42 ] da length = 4

Aus diesem Grund funktioniert fill() auch ganz hervorragend mit Nicht- bzw. Fake-Arrays mit einer Fake-Length:

// Kein Array mit einer Length, die mit nichts korrespondiert
let fake = { length: 7 };

// fill() für's Fake ausleihen
[].fill.call(fake, 42, 0, 7);

// fake = { 0: 42, 1: 42, 2: 42, 3: 42, 4: 42, 5: 42, 6: 42, length: 7 }

Das vielseitige fill() hat nur einen Haken: es befüllt Arrays (und alles, was ausreichend array-ähnlich ist, also eine length hat) mit einem einzigen, festen Wert. Falls es für jeden Index ein eigener Wert sein soll, hilft Array.from():

Array.from({ length: 7 }, (_, i) => i);
// > [ 0, 1, 2, 3, 4, 5, 6 ]

Array.from() macht aus seinem Input ein Array indem entweder der Iterator-Mechanismus des Inputs angestoßen wird (falls vorhanden) oder es als Fake-Array (mit length als maßgeblichem Merkmal) behandelt wird. Da Array.from() außerdem eine optionale Map-Funktion akzeptiert, ist es ein Leichtes, den Inhalt des Output-Arrays zu gestalten. Im obigen Beispiel verwenden wir als Inhalt den Index der (nicht-existenten) „Werte“ des Input-Fake-Arrays und schon haben wir eine mit 0 beginnende Zahlensequenz geschaffen! Mit minimal mehr Code können wir uns Arrays mit beliebigen Sequenzen erzeugen lassen:

const sequence = (from, to) => {
  return Array.from({ length: 1 + to - from }, (_, i) => i + from);
};
let numbers = sequence(5, 9);
// > [ 5, 6, 7, 8, 9 ]

Der einzige Makel der sequence()-Funktion ist, dass sie nur endliche Sequenzen erzeugen kann, da sie direkt ein fertig befülltes Array liefert und Computer (Stand 2021) immer noch die unangenehme Eigenschaft haben, über endliche Mengen an Speicher zu verfügen. Generator Functions können da helfen:

const range = function* (from, to) {
  while (from <= to) {
    yield from++;
  }
};

Der von range() erzeugte Generator kann entweder bei Bedarf stückweise Zahlen abrufen oder in Array.from() gesteckt und in ein Array verwandelt werden. Letzteres setzt natürlich voraus, dass die Sequenz endlich ist:

// Unendliche Zahlenreihe
const infinite = range(0, Infinity);

// Kein Problem
setTimeout(function loop () {
  console.log(infinite.next().value);
  setTimeout(loop, 1000);
}, 1000);

// Tilt!
const allNumbersEver = Array.from(infinite);
console.log(allNumbersEver)

Leider ist Array.from() von sich so überzeugt, dass es neben Arrays, array-ähnlichen Objekten und allen Objekten mit Iterator-Implementierung auch alles zu konsumieren versucht, das endlos ist – und sich dabei natürlich hoffnungslos überfrisst. Aber das lässt sich nicht verhindern; die Unendlichkeit einer unendlichen Sequenz ist nicht vor dem Abruf unendlich vieler Einträge aus der Sequenz absehbar. Die unendliche Sequenz ist ein Sonderfall, bei dem Array.from() auch nicht zur Array-Initialisierung taugt, wobei das Problem eher ist, dass Arrays an sich nicht gut zur Unendlichkeit passen.

Zusammengefasst können wir sagen, dass es eine Menge Möglichkeiten gibt, in JavaScript Arrays heraufzubeschwören. Das Array-Literal ist der einfachste Weg und durch eine globale Variable können wir unsere Bundle-Größe minimal heruntergolfen. Ergänzend bietet Array.of() die Funktionalität eines Array Literals in Form einer Funktion an. Array-Lengths können lügen und eine Belegung von Feldern vorgeben, die überhaupt nicht vorhanden ist. Da aber auch lügende Arrays nützlich sein können (z.B. für die Weiterverarbeitung durch fill()), können wir sie erschaffen, indem wir bei einem leeren Array die length auf den Zielwert setzen. Falls wir damit leben können, dass ein Array tatsächlich bei Initialisierung die Felder hat, die die length behauptet, ist Array.from({ length: x }) (mit der Ziel-Anzahl der Felder für x) das Mittel der Wahl. Da Array.from({ length: x }) als zweiten Parameter eine Mapping-Funktion akzeptiert, können wir dem generierten Array pro Feld variablen Inhalt verpassen, wohingegen fill() nur einen statischen Wert in einen gegebenen Bereich hineinschreibt. On-Demand-Erzeugung von Sequenzen ist das Fachgebiet von Generator Functions, deren Resultate sich per Array.from() (sofern endlich) in Arrays überführen lassen.

Das Wichtigste ist: in jedem Fall können wir von new Array() die Finger lassen. Denn das kann zwar (vom On-Demand-Aspekt abgesehen) so gut wie alles leisten, was wir in diesem Artikel besprochen haben, aber bietet all seine Fähigkeiten auf die jeweils unbequemste und fallenstellerischste Weise an. Außerhalb von Witzen über und Quizfragen zu JavaScript gibt es Stand 2021 keine Use Cases für new Array().