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

32 Typed Arrays: binäre Daten verarbeiten (fortgeschritten)



32.1 Die Grundlagen der API

Viele Daten im Web sind Text: JSON-Dateien, HTML-Dateien, CSS-Dateien, JavaScript-Code usw. JavaScript verarbeitet solche Daten gut über seine integrierten Strings.

Vor 2011 gab es jedoch keine gute Handhabung von binären Daten. Die Typed Array Specification 1.0 wurde am 8. Februar 2011 eingeführt und bietet Werkzeuge zur Arbeit mit binären Daten. Mit ECMAScript 6 wurden Typed Arrays in die Kernsprache aufgenommen und erhielten Methoden, die zuvor nur für normale Arrays verfügbar waren (.map(), .filter() usw.).

32.1.1 Anwendungsfälle für Typed Arrays

Die Hauptanwendungsfälle für Typed Arrays sind:

32.1.2 Die Kernklassen: ArrayBuffer, Typed Arrays, DataView

Die Typed Array API speichert binäre Daten in Instanzen von ArrayBuffer.

const buf = new ArrayBuffer(4); // length in bytes
  // buf is initialized with zeros

Ein ArrayBuffer selbst ist eine Blackbox: Wenn Sie auf seine Daten zugreifen möchten, müssen Sie ihn in ein anderes Objekt – ein View-Objekt – einwickeln. Zwei Arten von View-Objekten sind verfügbar:

Abb. 20 zeigt ein Klassendiagramm der API.

Figure 20: The classes of the Typed Array API.

32.1.3 Verwendung von Typed Arrays

Typed Arrays werden ähnlich wie normale Arrays verwendet, mit einigen bemerkenswerten Unterschieden:

32.1.3.1 Erstellen von Typed Arrays

Der folgende Code zeigt drei verschiedene Möglichkeiten, dasselbe Typed Array zu erstellen:

// Argument: Typed Array or Array-like object
const ta1 = new Uint8Array([0, 1, 2]);

const ta2 = Uint8Array.of(0, 1, 2);

const ta3 = new Uint8Array(3); // length of Typed Array
ta3[0] = 0;
ta3[1] = 1;
ta3[2] = 2;

assert.deepEqual(ta1, ta2);
assert.deepEqual(ta1, ta3);
32.1.3.2 Der gewrappte ArrayBuffer
const typedArray = new Int16Array(2); // 2 elements
assert.equal(typedArray.length, 2);

assert.deepEqual(
  typedArray.buffer, new ArrayBuffer(4)); // 4 bytes
32.1.3.3 Elemente abrufen und setzen
const typedArray = new Int16Array(2);

assert.equal(typedArray[1], 0); // initialized with 0
typedArray[1] = 72;
assert.equal(typedArray[1], 72);

32.1.4 Verwendung von DataViews

So werden DataViews verwendet:

const dataView = new DataView(new ArrayBuffer(4));
assert.equal(dataView.getInt16(0), 0);
assert.equal(dataView.getUint8(0), 0);
dataView.setUint8(0, 5);

32.2 Elementtypen

Tabelle 20: Von der Typed Array API unterstützte Elementtypen.
Element Typed Array Bytes Beschreibung
Int8 Int8Array 1 8-Bit vorzeichenbehafteter Integer ES6
Uint8 Uint8Array 1 8-Bit vorzeichenloser Integer ES6
Uint8C Uint8ClampedArray 1 8-Bit vorzeichenloser Integer ES6
(begrenzte Konvertierung) ES6
Int16 Int16Array 2 16-Bit vorzeichenbehafteter Integer ES6
Uint16 Uint16Array 2 16-Bit vorzeichenloser Integer ES6
Int32 Int32Array 4 32-Bit vorzeichenbehafteter Integer ES6
Uint32 Uint32Array 4 32-Bit vorzeichenloser Integer ES6
BigInt64 BigInt64Array 8 64-Bit vorzeichenbehafteter Integer ES2020
BigUint64 BigUint64Array 8 64-Bit vorzeichenloser Integer ES2020
Float32 Float32Array 4 32-Bit Gleitkommazahl ES6
Float64 Float64Array 8 64-Bit Gleitkommazahl ES6

Tab. 20 listet die verfügbaren Elementtypen auf. Diese Typen (z.B. Int32) erscheinen an zwei Stellen:

Der Elementtyp Uint8C ist besonders: Er wird nicht von DataView unterstützt und existiert nur, um Uint8ClampedArray zu ermöglichen. Dieses Typed Array wird vom canvas-Element verwendet (wo es CanvasPixelArray ersetzt) und sollte ansonsten vermieden werden. Der einzige Unterschied zwischen Uint8C und Uint8 liegt in der Behandlung von Über- und Unterlauf (wie im nächsten Unterabschnitt erklärt).

Typed Arrays und Array Buffers verwenden Zahlen und BigInts zum Importieren und Exportieren von Werten.

32.2.1 Behandlung von Über- und Unterlauf

Normalerweise wird, wenn ein Wert außerhalb des Bereichs des Elementtyps liegt, die Modulo-Arithmetik verwendet, um ihn in einen Wert innerhalb des Bereichs umzuwandeln. Für vorzeichenbehaftete und vorzeichenlose Integer bedeutet dies, dass:

Die folgende Funktion hilft, die Umwandlung zu veranschaulichen:

function setAndGet(typedArray, value) {
  typedArray[0] = value;
  return typedArray[0];
}

Modulo-Umwandlung für vorzeichenlose 8-Bit-Integer

const uint8 = new Uint8Array(1);

// Highest value of range
assert.equal(setAndGet(uint8, 255), 255);
// Overflow
assert.equal(setAndGet(uint8, 256), 0);

// Lowest value of range
assert.equal(setAndGet(uint8, 0), 0);
// Underflow
assert.equal(setAndGet(uint8, -1), 255);

Modulo-Umwandlung für vorzeichenbehaftete 8-Bit-Integer

const int8 = new Int8Array(1);

// Highest value of range
assert.equal(setAndGet(int8, 127), 127);
// Overflow
assert.equal(setAndGet(int8, 128), -128);

// Lowest value of range
assert.equal(setAndGet(int8, -128), -128);
// Underflow
assert.equal(setAndGet(int8, -129), 127);

Die begrenzende Umwandlung ist anders:

const uint8c = new Uint8ClampedArray(1);

// Highest value of range
assert.equal(setAndGet(uint8c, 255), 255);
// Overflow
assert.equal(setAndGet(uint8c, 256), 255);

// Lowest value of range
assert.equal(setAndGet(uint8c, 0), 0);
// Underflow
assert.equal(setAndGet(uint8c, -1), 0);

32.2.2 Endianness

Immer wenn ein Typ (wie Uint16) als eine Sequenz von mehreren Bytes gespeichert wird, ist die Endianness wichtig:

Die Endianness ist tendenziell pro CPU-Architektur festgelegt und über native APIs hinweg konsistent. Typed Arrays werden verwendet, um mit diesen APIs zu kommunizieren, weshalb ihre Endianness der Endianness der Plattform folgt und nicht geändert werden kann.

Andererseits variiert die Endianness von Protokollen und Binärdateien, ist aber pro Format plattformübergreifend festgelegt. Daher müssen wir in der Lage sein, auf Daten mit beiden Endianness zuzugreifen. DataViews dienen diesem Zweck und ermöglichen es Ihnen, die Endianness anzugeben, wenn Sie einen Wert abrufen oder setzen.

Zitat von Wikipedia zu Endianness::

Andere Reihenfolgen sind ebenfalls möglich. Diese werden allgemein als Middle-Endian oder Mixed-Endian bezeichnet.

32.3 Weitere Informationen zu Typed Arrays

In diesem Abschnitt steht «ElementType»Array für Int8Array, Uint8Array usw. ElementType steht für Int8, Uint8 usw.

32.3.1 Die statische Methode «ElementType»Array.from()

Diese Methode hat die Typsignatur:

.from<S>(
  source: Iterable<S>|ArrayLike<S>,
  mapfn?: S => ElementType, thisArg?: any)
  : «ElementType»Array

.from() konvertiert source in eine Instanz von this (ein Typed Array).

Zum Beispiel sind normale Arrays iterierbar und können mit dieser Methode konvertiert werden:

assert.deepEqual(
  Uint16Array.from([0, 1, 2]),
  Uint16Array.of(0, 1, 2));

Typed Arrays sind ebenfalls iterierbar:

assert.deepEqual(
  Uint16Array.from(Uint8Array.of(0, 1, 2)),
  Uint16Array.of(0, 1, 2));

source kann auch ein Array-ähnliches Objekt sein.

assert.deepEqual(
  Uint16Array.from({0:0, 1:1, 2:2, length: 3}),
  Uint16Array.of(0, 1, 2));

Das optionale mapfn ermöglicht es Ihnen, die Elemente von source zu transformieren, bevor sie zu Elementen des Ergebnisses werden. Warum die beiden Schritte Mapping und Konvertierung in einem Schritt durchführen? Im Vergleich zum separaten Mapping über .map() gibt es zwei Vorteile:

  1. Es wird kein intermediäres Array oder Typed Array benötigt.
  2. Bei der Konvertierung zwischen Typed Arrays mit unterschiedlichen Präzisionen kann weniger schiefgehen.

Lesen Sie weiter für eine Erklärung des zweiten Vorteils.

32.3.1.1 Fallstrick: Mapping während der Konvertierung zwischen Typed Array-Typen

Die statische Methode .from() kann optional sowohl Mappen als auch zwischen Typed Array-Typen konvertieren. Weniger kann schiefgehen, wenn Sie diese Methode verwenden.

Um zu verstehen, warum das so ist, konvertieren wir zuerst ein Typed Array in ein Typed Array mit höherer Präzision. Wenn wir .from() zum Mappen verwenden, ist das Ergebnis automatisch korrekt. Andernfalls müssen Sie zuerst konvertieren und dann mappen.

const typedArray = Int8Array.of(127, 126, 125);
assert.deepEqual(
  Int16Array.from(typedArray, x => x * 2),
  Int16Array.of(254, 252, 250));

assert.deepEqual(
  Int16Array.from(typedArray).map(x => x * 2),
  Int16Array.of(254, 252, 250)); // OK
assert.deepEqual(
  Int16Array.from(typedArray.map(x => x * 2)),
  Int16Array.of(-2, -4, -6)); // wrong

Wenn wir von einem Typed Array zu einem Typed Array mit niedrigerer Präzision wechseln, erzeugt das Mapping über .from() das korrekte Ergebnis. Andernfalls müssen wir zuerst mappen und dann konvertieren.

assert.deepEqual(
  Int8Array.from(Int16Array.of(254, 252, 250), x => x / 2),
  Int8Array.of(127, 126, 125));

assert.deepEqual(
  Int8Array.from(Int16Array.of(254, 252, 250).map(x => x / 2)),
  Int8Array.of(127, 126, 125)); // OK
assert.deepEqual(
  Int8Array.from(Int16Array.of(254, 252, 250)).map(x => x / 2),
  Int8Array.of(-1, -2, -3)); // wrong

Das Problem ist, dass beim Mapping über .map() der Eingabetyp und der Ausgabetyp gleich sind. Im Gegensatz dazu geht .from() von einem beliebigen Eingabetyp zu einem Ausgabetyp über, den Sie über seinen Empfänger angeben.

32.3.2 Typed Arrays sind iterierbar

Typed Arrays sind iterierbar. Das bedeutet, dass Sie die for-of-Schleife und andere iterationsbasierte Mechanismen verwenden können:

const ui8 = Uint8Array.of(0, 1, 2);
for (const byte of ui8) {
  console.log(byte);
}
// Output:
// 0
// 1
// 2

ArrayBuffers und DataViews sind nicht iterierbar.

32.3.3 Typed Arrays vs. normale Arrays

Typed Arrays ähneln stark normalen Arrays: Sie haben eine .length, Elemente können über den Klammeroperator [] abgerufen werden, und sie haben die meisten Standard-Array-Methoden. Sie unterscheiden sich von normalen Arrays in folgenden Punkten:

32.3.4 Konvertierung von Typed Arrays zu und von normalen Arrays

Um ein normales Array in ein Typed Array zu konvertieren, übergeben Sie es an einen Typed Array-Konstruktor (der Array-ähnliche Objekte und Typed Arrays akzeptiert) oder an «ElementType»Array.from() (der Iterables und Array-ähnliche Objekte akzeptiert). Zum Beispiel:

const ta1 = new Uint8Array([0, 1, 2]);
const ta2 = Uint8Array.from([0, 1, 2]);
assert.deepEqual(ta1, ta2);

Um ein Typed Array in ein normales Array zu konvertieren, können Sie Array.from() oder Spread-Syntax verwenden (da Typed Arrays iterierbar sind):

assert.deepEqual(
  [...Uint8Array.of(0, 1, 2)], [0, 1, 2]
);
assert.deepEqual(
  Array.from(Uint8Array.of(0, 1, 2)), [0, 1, 2]
);

32.3.5 Verketten von Typed Arrays

Typed Arrays haben keine Methode .concat() wie normale Arrays. Die Umgehung besteht darin, ihre überladene Methode .set() zu verwenden:

.set(typedArray: TypedArray, offset=0): void
.set(arrayLike: ArrayLike<number>, offset=0): void

Sie kopiert das vorhandene typedArray oder arrayLike in den Empfänger an Index offset. TypedArray ist eine fiktive abstrakte Oberklasse aller konkreten Typed Array-Klassen.

Die folgende Funktion verwendet diese Methode, um null oder mehr Typed Arrays (oder Array-ähnliche Objekte) in eine Instanz von resultConstructor zu kopieren:

function concatenate(resultConstructor, ...arrays) {
  let totalLength = 0;
  for (const arr of arrays) {
    totalLength += arr.length;
  }
  const result = new resultConstructor(totalLength);
  let offset = 0;
  for (const arr of arrays) {
    result.set(arr, offset);
    offset += arr.length;
  }
  return result;
}
assert.deepEqual(
  concatenate(Uint8Array, Uint8Array.of(1, 2), [3, 4]),
  Uint8Array.of(1, 2, 3, 4));

32.4 Schnellreferenz: Indizes vs. Offsets

Zur Vorbereitung auf die Schnellreferenzen zu ArrayBuffers, Typed Arrays und DataViews müssen wir die Unterschiede zwischen Indizes und Offsets lernen:

Ob ein Parameter ein Index oder ein Offset ist, kann nur durch Nachschlagen in der Dokumentation ermittelt werden; es gibt keine einfache Regel.

32.5 Schnellreferenz: ArrayBuffers

ArrayBuffers speichern binäre Daten, die über Typed Arrays und DataViews zugegriffen werden sollen.

32.5.1 new ArrayBuffer()

Die Typsignatur des Konstruktors lautet:

new ArrayBuffer(length: number)

Die Ausführung dieses Konstruktors mit new erstellt eine Instanz, deren Kapazität length Bytes beträgt. Jedes dieser Bytes ist anfänglich 0.

Sie können die Länge eines ArrayBuffers nicht ändern; Sie können nur einen neuen mit einer anderen Länge erstellen.

32.5.2 Statische Methoden von ArrayBuffer

32.5.3 Eigenschaften von ArrayBuffer.prototype

32.6 Schnellreferenz: Typed Arrays

Die Eigenschaften der verschiedenen Typed Array-Objekte werden in zwei Schritten eingeführt:

  1. TypedArray: Zuerst betrachten wir die abstrakte Oberklasse aller Typed Array-Klassen (die im Klassendiagramm zu Beginn dieses Kapitels gezeigt wurde). Ich nenne diese Oberklasse TypedArray, aber sie ist von JavaScript aus nicht direkt zugänglich. TypedArray.prototype enthält alle Methoden von Typed Arrays.
  2. «ElementType»Array: Die konkreten Typed Array-Klassen heißen Uint8Array, Int16Array, Float32Array usw. Dies sind die Klassen, die Sie über new, .of und .from() verwenden.

32.6.1 Statische Methoden von TypedArray<T>

Beide statischen TypedArray-Methoden werden von ihren Unterklassen (Uint8Array usw.) geerbt. TypedArray ist abstrakt. Daher verwenden Sie diese Methoden immer über die Unterklassen, die konkret sind und direkte Instanzen haben können.

32.6.2 Eigenschaften von TypedArray<T>.prototype

Von Typed Array-Methoden akzeptierte Indizes können negativ sein (sie funktionieren auf diese Weise wie traditionelle Array-Methoden). Offsets müssen nicht-negativ sein. Details finden Sie unter §32.4 „Schnellreferenz: Indizes vs. Offsets“.

32.6.2.1 Spezifische Eigenschaften für Typed Arrays

Die folgenden Eigenschaften sind spezifisch für Typed Arrays; normale Arrays haben sie nicht:

32.6.2.2 Array-Methoden

Die folgenden Methoden sind im Grunde gleich wie die Methoden von normalen Arrays:

Details zur Funktionsweise dieser Methoden finden Sie unter §31.13.3 „Methoden von Array.prototype.

32.6.3 new «ElementType»Array()

Jeder Typed Array-Konstruktor hat einen Namen, der dem Muster «ElementType»Array folgt, wobei «ElementType» einer der Elementtypen in der Tabelle am Anfang ist. Das bedeutet, dass es 11 Konstruktoren für Typed Arrays gibt:

Jeder Konstruktor hat vier überladene Versionen – er verhält sich unterschiedlich, abhängig von der Anzahl der Argumente und deren Typen:

32.6.4 Statische Eigenschaften von «ElementType»Array

32.6.5 Eigenschaften von «ElementType»Array.prototype

32.7 Schnellreferenz: DataViews

32.7.1 new DataView()

32.7.2 Eigenschaften von DataView.prototype

Im Rest dieses Abschnitts bezieht sich «ElementType» auf entweder:

Dies sind die Eigenschaften von DataView.prototype: