20. Typed Arrays
Inhaltsverzeichnis
Bitte unterstützen Sie dieses Buch: kaufen Sie es (PDF, EPUB, MOBI) oder spenden Sie
(Werbung, bitte nicht blockieren.)

20. Typed Arrays



20.1 Übersicht

Typed Arrays sind eine ECMAScript 6 API zur Verarbeitung von Binärdaten.

Code-Beispiel

const typedArray = new Uint8Array([0,1,2]);
console.log(typedArray.length); // 3
typedArray[0] = 5;
const normalArray = [...typedArray]; // [5,1,2]

// The elements are stored in typedArray.buffer.
// Get a different view on the same data:
const dataView = new DataView(typedArray.buffer);
console.log(dataView.getUint8(0)); // 5

Instanzen von ArrayBuffer speichern die zu verarbeitenden Binärdaten. Zwei Arten von *Views* werden verwendet, um auf die Daten zuzugreifen:

Die folgenden Browser-APIs unterstützen Typed Arrays (Details sind in einem eigenen Abschnitt aufgeführt).

20.2 Einführung

Viele Daten, denen man im Web begegnet, sind Text: JSON-Dateien, HTML-Dateien, CSS-Dateien, JavaScript-Code usw. Für die Verarbeitung solcher Daten ist der integrierte String-Datentyp von JavaScript gut geeignet. Bis vor wenigen Jahren war JavaScript jedoch schlecht für die Verarbeitung von Binärdaten ausgestattet. Am 8. Februar 2011 standardisierte die Typed Array Specification 1.0 Einrichtungen für die Verarbeitung von Binärdaten. Mittlerweile werden Typed Arrays von verschiedenen Engines gut unterstützt. Mit ECMAScript 6 wurden sie Teil der Kernsprache und erhielten dabei viele Methoden, die zuvor nur für Arrays verfügbar waren (map(), filter() usw.).

Die Hauptanwendungsfälle für Typed Arrays sind:

Zwei Arten von Objekten arbeiten in der Typed Array API zusammen:

Dies ist ein Diagramm der Struktur der Typed Array API (bemerkenswert: alle Typed Arrays haben eine gemeinsame Oberklasse).

20.2.1 Elementtypen

Die folgenden Elementtypen werden von der API unterstützt:

Elementtyp Bytes Beschreibung C-Typ
Int8 1 8-Bit vorzeichenbehafteter Integer signed char
Uint8 1 8-Bit vorzeichenloser Integer unsigned char
Uint8C 1 8-Bit vorzeichenloser Integer (begrenzte Konvertierung) unsigned char
Int16 2 16-Bit vorzeichenbehafteter Integer short
Uint16 2 16-Bit vorzeichenloser Integer unsigned short
Int32 4 32-Bit vorzeichenbehafteter Integer int
Uint32 4 32-Bit vorzeichenloser Integer unsigned int
Float32 4 32-Bit Gleitkommazahl float
Float64 8 64-Bit Gleitkommazahl double

Der Elementtyp Uint8C ist besonders: Er wird nicht von DataView unterstützt und existiert nur, um Uint8ClampedArray zu ermöglichen. Dieser Typed Array wird vom canvas-Element verwendet (wo er CanvasPixelArray ersetzt). Der einzige Unterschied zwischen Uint8C und Uint8 liegt darin, wie Über- und Unterläufe behandelt werden (wie im nächsten Abschnitt erklärt). Es wird empfohlen, ersteren zu vermeiden – Zitat von Brendan Eich

Nur um ganz klar zu sein (und ich war dabei, als es geboren wurde), Uint8ClampedArray ist *absolut* ein historisches Artefakt (des HTML5 Canvas-Elements). Vermeiden Sie es, es sei denn, Sie arbeiten wirklich mit Canvas-artigen Dingen.

20.2.2 Umgang mit Über- und Unterlauf

Normalerweise wird beim Überschreiten des Wertebereichs des Elementtyps eine Modulo-Arithmetik verwendet, um ihn in einen Wert innerhalb des Bereichs zu konvertieren. Für vorzeichenbehaftete und vorzeichenlose Integer bedeutet dies, dass:

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

> const uint8 = new Uint8Array(1);
> uint8[0] = 255; uint8[0] // highest value within range
255
> uint8[0] = 256; uint8[0] // overflow
0
> uint8[0] = 0; uint8[0] // lowest value within range
0
> uint8[0] = -1; uint8[0] // underflow
255

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

> const int8 = new Int8Array(1);
> int8[0] = 127; int8[0] // highest value within range
127
> int8[0] = 128; int8[0] // overflow
-128
> int8[0] = -128; int8[0] // lowest value within range
-128
> int8[0] = -129; int8[0] // underflow
127

Die begrenzte Konvertierung ist anders:

> const uint8c = new Uint8ClampedArray(1);
> uint8c[0] = 255; uint8c[0] // highest value within range
255
> uint8c[0] = 256; uint8c[0] // overflow
255
> uint8c[0] = 0; uint8c[0] // lowest value within range
0
> uint8c[0] = -1; uint8c[0] // underflow
0

20.2.3 Endianness

Immer wenn ein Typ (wie Uint16) als mehrere Bytes gespeichert wird, spielt die *Endianness* eine Rolle:

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

Andererseits variiert die Endianness von Protokollen und Binärdateien und ist plattformübergreifend festgelegt. Daher müssen wir in der Lage sein, Daten mit beiden Endianness-Arten zu lesen. DataViews dienen diesem Zweck und ermöglichen es Ihnen, die Endianness beim Abrufen oder Festlegen eines Werts anzugeben.

Zitat von Wikipedia über Endianness:

Sie können die folgende Funktion verwenden, um die Endianness einer Plattform zu bestimmen.

const BIG_ENDIAN = Symbol('BIG_ENDIAN');
const LITTLE_ENDIAN = Symbol('LITTLE_ENDIAN');
function getPlatformEndianness() {
    const arr32 = Uint32Array.of(0x12345678);
    const arr8 = new Uint8Array(arr32.buffer);
    switch ((arr8[0]*0x1000000) + (arr8[1]*0x10000) + (arr8[2]*0x100) + (arr8\
[3])) {
        case 0x12345678:
            return BIG_ENDIAN;
        case 0x78563412:
            return LITTLE_ENDIAN;
        default:
            throw new Error('Unknown endianness');
    }
}

Es gibt auch Plattformen, die *Wörter* (Byte-Paare) mit einer anderen Endianness als Bytes innerhalb von Wörtern anordnen. Dies wird als gemischte Endianness bezeichnet. Sollten Sie eine solche Plattform unterstützen wollen, lässt sich der obige Code leicht erweitern.

20.2.4 Negative Indizes

Mit dem Klammeroperator [ ] können nur nicht-negative Indizes (beginnend bei 0) verwendet werden. Die Methoden von ArrayBuffers, Typed Arrays und DataViews verhalten sich anders: Jeder Index kann negativ sein. Wenn er negativ ist, zählt er rückwärts von der Länge. Mit anderen Worten, er wird zur Länge addiert, um einen normalen Index zu erzeugen. Daher bezieht sich -1 auf das letzte Element, -2 auf das vorletzte usw. Methoden normaler Arrays verhalten sich genauso.

> const ui8 = Uint8Array.of(0, 1, 2);
> ui8.slice(-1)
Uint8Array [ 2 ]

Offsets hingegen müssen nicht-negativ sein. Wenn Sie zum Beispiel -1 an

DataView.prototype.getInt8(byteOffset)

übergeben, erhalten Sie einen RangeError.

20.3 ArrayBuffers

ArrayBuffers speichern die Daten, *Views* (Typed Arrays und DataViews) erlauben Ihnen, diese zu lesen und zu ändern. Um eine DataView zu erstellen, müssen Sie deren Konstruktor einen ArrayBuffer übergeben. Typed Array-Konstruktoren können optional einen ArrayBuffer für Sie erstellen.

20.3.1 ArrayBuffer Konstruktor

Die Signatur des Konstruktors lautet:

ArrayBuffer(length : number)

Die Ausführung dieses Konstruktors über new erzeugt eine Instanz mit einer Kapazität von length Bytes. Jedes dieser Bytes ist anfänglich 0.

20.3.2 Statische ArrayBuffer Methoden

20.3.3 ArrayBuffer.prototype Eigenschaften

20.4 Typed Arrays

Die verschiedenen Arten von Typed Arrays unterscheiden sich nur hinsichtlich des Typs ihrer Elemente.

20.4.1 Typed Arrays im Vergleich zu normalen Arrays

Typed Arrays sind normalen Arrays sehr ähnlich: Sie haben eine length, Elemente können über den Klammeroperator [ ] abgerufen werden und sie verfügen über alle Standard-Array-Methoden. Sie unterscheiden sich von Arrays in folgenden Punkten:

20.4.2 Typed Arrays sind iterierbar

Typed Arrays implementieren eine Methode mit dem Schlüssel Symbol.iterator und sind daher iterierbar (siehe Kapitel „Iterables und Iteratoren“ für weitere Informationen). Das bedeutet, dass Sie die for-of-Schleife und ähnliche Mechanismen in ES6 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.

20.4.3 Konvertieren von Typed Arrays zu und von normalen Arrays

Um ein normales Array in ein Typed Array zu konvertieren, machen Sie es zum Parameter eines Typed Array-Konstruktors. Zum Beispiel:

> const tarr = new Uint8Array([0,1,2]);

Die klassische Methode, ein Typed Array in ein Array zu konvertieren, ist die Ausführung von Array.prototype.slice darauf. Dieser Trick funktioniert für alle Array-ähnlichen Objekte (wie arguments) und Typed Arrays sind Array-ähnlich.

> Array.prototype.slice.call(tarr)
[ 0, 1, 2 ]

In ES6 können Sie den Spread-Operator (...) verwenden, da Typed Arrays iterierbar sind.

> [...tarr]
[ 0, 1, 2 ]

Eine weitere ES6-Alternative ist Array.from(), das mit Iterables oder Array-ähnlichen Objekten funktioniert.

> Array.from(tarr)
[ 0, 1, 2 ]

20.4.4 Das Species-Muster für Typed Arrays

Einige Methoden erstellen neue Instanzen, die this ähneln. Das Species-Muster ermöglicht es Ihnen zu konfigurieren, welcher Konstruktor dazu verwendet werden soll. Wenn Sie beispielsweise eine Unterklasse MyArray von Array erstellen, ist standardmäßig map() Instanzen von MyArray erstellt. Wenn Sie möchten, dass es Instanzen von Array erstellt, können Sie das Species-Muster verwenden, um dies zu erreichen. Details werden in Abschn. „Das Species-Muster“ im Kapitel über Klassen erläutert.

ArrayBuffers verwenden das Species-Muster an folgenden Stellen:

Typed Arrays verwenden das Species-Muster an folgenden Stellen:

DataViews verwenden das Species-Muster nicht.

20.4.5 Die Vererbungshierarchie von Typed Arrays

Wie Sie im Diagramm am Anfang dieses Kapitels sehen konnten, haben alle Typed Array-Klassen (Uint8Array usw.) eine gemeinsame Oberklasse. Ich nenne diese Oberklasse TypedArray, aber sie ist von JavaScript aus nicht direkt zugänglich (die ES6-Spezifikation nennt sie *das intrinsische Objekt %TypedArray%*). TypedArray.prototype enthält alle Methoden von Typed Arrays.

20.4.6 Statische TypedArray Methoden

Beide statischen TypedArray-Methoden werden von ihren Unterklassen (Uint8Array usw.) geerbt.

20.4.6.1 TypedArray.of()

Diese Methode hat die Signatur:

TypedArray.of(...items)

Sie erstellt ein neues Typed Array, das eine Instanz von this ist (die Klasse, auf der of() aufgerufen wurde). Die Elemente dieser Instanz sind die Parameter von of().

Sie können of() als eine benutzerdefinierte Literalschreibweise für Typed Arrays betrachten.

> Float32Array.of(0.151, -8, 3.7)
Float32Array [ 0.151, -8, 3.7 ]
20.4.6.2 TypedArray.from()

Diese Methode hat die Signatur:

TypedArray<U>.from(source : Iterable<T>, mapfn? : T => U, thisArg?)

Sie konvertiert das Iterable source in eine Instanz von this (ein Typed Array).

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

> Uint16Array.from([0, 1, 2])
Uint16Array [ 0, 1, 2 ]

Typed Arrays sind ebenfalls iterierbar.

> const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
> ui16 instanceof Uint16Array
true

Die 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 ausführen? Verglichen mit der separaten Ausführung des ersten Schritts über source.map() gibt es zwei Vorteile:

  1. Kein separates Array oder Typed Array ist erforderlich.
  2. Beim Konvertieren eines Typed Arrays in ein Typed Array, dessen Elemente eine höhere Präzision haben, kann der Mapping-Schritt diese höhere Präzision nutzen.

Um den zweiten Vorteil zu veranschaulichen, verwenden wir map(), um die Elemente eines Typed Arrays zu verdoppeln:

> Int8Array.of(127, 126, 125).map(x => 2 * x)
Int8Array [ -2, -4, -6 ]

Wie Sie sehen, kommt es zu Überläufen und die Werte werden in den Wertebereich von Int8 umgewandelt. Wenn Sie über from() mappen, können Sie den Typ des Ergebnisses so wählen, dass die Werte nicht überlaufen:

> Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)
Int16Array [ 254, 252, 250 ]

Laut Allen Wirfs-Brock war das Mappen zwischen Typed Arrays der Grund für den mapfn-Parameter von from().

20.4.7 TypedArray.prototype Eigenschaften

Von Typed Array-Methoden akzeptierte Indizes können negativ sein (sie verhalten sich auf diese Weise wie traditionelle Array-Methoden). Offsets müssen nicht-negativ sein. Details finden Sie in Abschn. „Negative Indizes“.

20.4.7.1 Spezifische Methoden für Typed Arrays

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

20.4.7.2 Array-Methoden

Die folgenden Methoden sind im Grunde dieselben wie die Methoden normaler Arrays:

Aufgrund der Tatsache, dass all diese Methoden für Arrays verfügbar sind, können Sie die folgenden beiden Quellen konsultieren, um mehr darüber zu erfahren, wie sie funktionieren:

Beachten Sie, dass normale Array-Methoden zwar generisch sind (jeder Array-ähnliche this ist in Ordnung), die in diesem Abschnitt aufgeführten Methoden jedoch nicht (this muss ein Typed Array sein).

20.4.8 «ElementType»Array Konstruktor

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 9 Konstruktoren für Typed Arrays gibt: Int8Array, Uint8Array, Uint8ClampedArray (Elementtyp Uint8C), Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array.

Jeder Konstruktor hat fünf *überladene* Versionen – er verhält sich unterschiedlich, je nachdem, wie viele Argumente er erhält und welche Typen diese haben:

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

const tarr1 = new Uint8Array([1,2,3]);

const tarr2 = Uint8Array.of(1,2,3);

const tarr3 = new Uint8Array(3);
tarr3[0] = 0;
tarr3[1] = 1;
tarr3[2] = 2;

20.4.9 Statische «ElementType»Array Eigenschaften

20.4.10 «ElementType»Array.prototype Eigenschaften

20.4.11 Konkatenieren von Typed Arrays

Typed Arrays haben keine concat()-Methode wie normale Arrays. Der Workaround besteht darin, die Methode zu verwenden

typedArray.set(arrayOrTypedArray, offset=0)

Diese Methode kopiert ein vorhandenes Typed Array (oder normales Array) in typedArray am Index offset. Dann müssen Sie nur sicherstellen, dass typedArray groß genug ist, um alle (Typed) Arrays aufzunehmen, die Sie konkatenieren möchten.

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;
}
console.log(concatenate(Uint8Array,
    Uint8Array.of(1, 2), Uint8Array.of(3, 4)));
        // Uint8Array [1, 2, 3, 4]

20.5 DataViews

20.5.1 DataView Konstruktor

20.5.2 DataView.prototype Eigenschaften

20.6 Browser-APIs, die Typed Arrays unterstützen

Typed Arrays gibt es schon seit einiger Zeit, daher gibt es eine ganze Reihe von Browser-APIs, die sie unterstützen.

20.6.1 File API

Die File API ermöglicht den Zugriff auf lokale Dateien. Der folgende Code zeigt, wie die Bytes einer übermittelten lokalen Datei in einem ArrayBuffer abgerufen werden können.

const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function () {
    const arrayBuffer = reader.result;
    ···
};

20.6.2 XMLHttpRequest

In neueren Versionen der XMLHttpRequest API können Sie die Ergebnisse in einem ArrayBuffer liefern lassen.

const xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
xhr.responseType = 'arraybuffer';

xhr.onload = function () {
    const arrayBuffer = xhr.response;
    ···
};

xhr.send();

20.6.3 Fetch API

Ähnlich wie XMLHttpRequest ermöglicht die Fetch API das Anfordern von Ressourcen. Sie basiert jedoch auf Promises, was die Verwendung erleichtert. Der folgende Code zeigt, wie der Inhalt, auf den url verweist, als ArrayBuffer heruntergeladen wird.

fetch(url)
.then(request => request.arrayBuffer())
.then(arrayBuffer => ···);

20.6.4 Canvas

Zitat der HTML5-Spezifikation::

Das canvas-Element stellt Skripten eine auflösungsabhängige Bitmap-Leinwand zur Verfügung, die zum Rendern von Grafiken, Spielgrafiken, Kunst oder anderen visuellen Bildern im Handumdrehen verwendet werden kann.

Der 2D-Kontext von canvas ermöglicht es Ihnen, die Bitmap-Daten als Instanz von Uint8ClampedArray abzurufen.

const canvas = document.getElementById('my_canvas');
const context = canvas.getContext('2d');
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const uint8ClampedArray = imageData.data;

20.6.5 WebSockets

WebSockets ermöglichen das Senden und Empfangen von Binärdaten über ArrayBuffers.

const socket = new WebSocket('ws://127.0.0.1:8081');
socket.binaryType = 'arraybuffer';

// Wait until socket is open
socket.addEventListener('open', function (event) {
    // Send binary data
    const typedArray = new Uint8Array(4);
    socket.send(typedArray.buffer);
});

// Receive binary data
socket.addEventListener('message', function (event) {
    const arrayBuffer = event.data;
    ···
});

20.6.6 Andere APIs

20.7 Erweitertes Beispiel: JPEG SOF0 Dekoder

Das Beispiel ist eine Webseite, auf der Sie eine JPEG-Datei hochladen und deren Struktur parsen können, um die Höhe und Breite des Bildes und mehr zu ermitteln.

20.7.1 Das JPEG-Dateiformat

Eine JPEG-Datei ist eine Abfolge von *Segmenten* (typisierte Daten). Jedes Segment beginnt mit den folgenden vier Bytes:

JPEG-Dateien sind auf allen Plattformen Big-Endian. Daher zeigt dieses Beispiel, wie wichtig es ist, dass wir beim Verwenden von DataViews die Endianness angeben können.

20.7.2 Der JavaScript-Code

Die folgende Funktion processArrayBuffer() ist eine gekürzte Version des tatsächlichen Codes; ich habe ein paar Fehlerprüfungen entfernt, um die Übersichtlichkeit zu verbessern. processArrayBuffer() empfängt einen ArrayBuffer mit dem Inhalt der übermittelten JPEG-Datei und iteriert über seine Segmente.

// JPEG is big endian
var IS_LITTLE_ENDIAN = false;

function processArrayBuffer(arrayBuffer) {
    try {
        var dv = new DataView(arrayBuffer);
        ···
        var ptr = 2;
        while (true) {
            ···
            var lastPtr = ptr;
            enforceValue(0xFF, dv.getUint8(ptr),
                'Not a marker');
            ptr++;
            var marker = dv.getUint8(ptr);
            ptr++;
            var len = dv.getUint16(ptr, IS_LITTLE_ENDIAN);
            ptr += len;
            logInfo('Marker: '+hex(marker)+' ('+len+' byte(s))');
            ···

            // Did we find what we were looking for?
            if (marker === 0xC0) { // SOF0
                logInfo(decodeSOF0(dv, lastPtr));
                break;
            }
        }
    } catch (e) {
        logError(e.message);
    }
}

Dieser Code verwendet die folgenden Hilfsfunktionen (die hier nicht gezeigt werden)

decodeSOF0() parst das Segment SOF0

function decodeSOF0(dv, start) {
    // Example (16x16):
    // FF C0 00 11 08 00 10 00 10 03 01 22 00 02 11 01 03 11 01
    var data = {};
    start += 4; // skip marker 0xFFC0 and segment length 0x0011
    var data = {
        bitsPerColorComponent: dv.getUint8(start), // usually 0x08
        imageHeight: dv.getUint16(start+1, IS_LITTLE_ENDIAN),
        imageWidth: dv.getUint16(start+3, IS_LITTLE_ENDIAN),
        numberOfColorComponents: dv.getUint8(start+5),
    };
    return JSON.stringify(data, null, 4);
}

Weitere Informationen zur Struktur von JPEG-Dateien

20.8 Verfügbarkeit

Ein Großteil der Typed Array API wird von allen modernen JavaScript-Engines implementiert, aber mehrere Funktionen sind neu in ECMAScript 6

Es kann eine Weile dauern, bis diese überall verfügbar sind. Wie üblich beschreibt kangax' „ES6-Kompatibilitätstabelle“ den Status quo.

Weiter: 21. Iterables und Iteratoren