20. 型付き配列
目次
本書をサポートしてください:購入 (PDF, EPUB, MOBI) または 寄付
(広告、ブロックしないでください。)

20. 型付き配列



20.1 概要

型付き配列は、バイナリデータを扱うためのECMAScript 6 APIです。

コード例

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

ArrayBufferのインスタンスは、処理されるバイナリデータを格納します。データにアクセスするには、2種類のビューが使用されます。

以下のブラウザAPIは型付き配列をサポートしています(詳細は専用のセクションで説明されています)。

20.2 はじめに

ウェブ上で遭遇するデータの多くはテキストです。JSONファイル、HTMLファイル、CSSファイル、JavaScriptコードなどです。このようなデータを処理するには、JavaScriptの組み込み文字列データ型がうまく機能します。しかし、数年前までは、JavaScriptはバイナリデータを処理するのに不向きでした。2011年2月8日、型付き配列仕様1.0はバイナリデータを処理するための機能を標準化しました。現在では、型付き配列はさまざまなエンジンで広くサポートされています。ECMAScript 6では、コア言語の一部になり、以前は配列(map()filter()など)に対してのみ利用可能だった多くのメソッドが追加されました。

型付き配列の主なユースケースは次のとおりです。

型付き配列APIでは、2種類のオブジェクトが連携して動作します。

これは、型付き配列APIの構造図です(注目点:すべての型付き配列には共通のスーパークラスがあります)。

20.2.1 要素の種類

APIでは、次の要素の種類がサポートされています。

要素の種類 バイト数 説明 C言語の型
Int8 1 8ビット符号付き整数 signed char
Uint8 1 8ビット符号なし整数 unsigned char
Uint8C 1 8ビット符号なし整数(クランプ変換) unsigned char
Int16 2 16ビット符号付き整数 short
Uint16 2 16ビット符号なし整数 unsigned short
Int32 4 32ビット符号付き整数 int
Uint32 4 32ビット符号なし整数 unsigned int
Float32 4 32ビット浮動小数点数 float
Float64 8 64ビット浮動小数点数 double

要素の種類Uint8Cは特殊です。DataViewではサポートされておらず、Uint8ClampedArrayを有効にするためだけに存在します。この型付き配列は、canvas要素で使用されます(ここでCanvasPixelArrayに取って代わります)。Uint8CUint8の違いは、オーバーフローとアンダーフローの処理方法だけです(次のセクションで説明します)。前者を使用しないことをお勧めします。-Brendan Eichの引用

念のため、(そして私が生まれたときのことを知っているから)、Uint8ClampedArray完全に歴史的な遺物です(HTML5 canvas要素の)。canvas関連の作業を本当にしている場合を除き、避けてください。

20.2.2 オーバーフローとアンダーフローの処理

通常、値が要素の種類の範囲外にある場合、剰余演算を使用して範囲内の値に変換します。符号付き整数と符号なし整数の場合は、次のようになります。

符号なし8ビット整数の剰余変換

> 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

符号付き8ビット整数の剰余変換

> 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

クランプ変換は異なります。

> 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 エンディアン

型(Uint16など)が複数のバイトとして格納される場合、エンディアンが重要になります。

エンディアンは、通常、CPUアーキテクチャごとに固定され、ネイティブAPI全体で一貫しています。型付き配列はこれらのAPIと通信するために使用されるため、エンディアンはプラットフォームのエンディアンに従い、変更できません。

一方、プロトコルとバイナリファイルのエンディアンは異なり、プラットフォーム全体で固定されています。そのため、どちらのエンディアンでもデータにアクセスできる必要があります。DataViewは、このユースケースに対応し、値を取得または設定する際にエンディアンを指定できます。

Wikipediaのエンディアンに関する引用:

プラットフォームのエンディアンを判別するには、次の関数を使用できます。

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');
    }
}

ワード(バイトのペア)を、ワード内のバイトとは異なるエンディアンで配置するプラットフォームもあります。これは、混合エンディアンと呼ばれます。そのようなプラットフォームをサポートする必要がある場合は、前のコードを簡単に拡張できます。

20.2.4 負のインデックス

ブラケット演算子[ ]では、非負のインデックス(0から始まる)のみを使用できます。ArrayBuffer、型付き配列、DataViewのメソッドは異なります。すべてのインデックスが負になる可能性があります。負の場合は、長さから逆算されます。言い換えると、長さに加算されて通常のインデックスが生成されます。したがって、-1は最後の要素を、-2は最後から2番目の要素を参照します。通常の配列のメソッドも同様に動作します。

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

一方、オフセットは非負でなければなりません。たとえば、次のように-1を渡すと

DataView.prototype.getInt8(byteOffset)

RangeErrorが発生します。

20.3 ArrayBuffer

ArrayBufferはデータを格納し、ビュー(型付き配列とDataView)を使用して読み書きします。DataViewを作成するには、コンストラクタにArrayBufferを提供する必要があります。型付き配列コンストラクタは、必要に応じてArrayBufferを作成することもできます。

20.3.1 ArrayBuffer コンストラクタ

コンストラクタのシグネチャは次のとおりです。

ArrayBuffer(length : number)

newを使用してこのコンストラクタを呼び出すと、容量がlengthバイトのインスタンスが作成されます。これらのバイトのそれぞれは、最初は0です。

20.3.2 静的ArrayBufferメソッド

20.3.3 ArrayBuffer.prototype プロパティ

20.4 型付き配列

様々な種類の Typed Array は、要素の型だけが異なります。

20.4.1 Typed Array と通常の Array の違い

Typed Array は通常の Array と非常によく似ています。`length` プロパティを持ち、ブラケット演算子 `[ ]` を使用して要素にアクセスでき、標準的な Array メソッドもすべて備えています。Array との違いは以下の通りです。

20.4.2 Typed Array は反復可能

Typed Array はキーが `Symbol.iterator` であるメソッドを実装しており、したがって反復可能です(詳細は「反復可能オブジェクトとイテレータ」を参照)。つまり、ES6 の `for-of` ループや同様のメカニズムを使用できます。

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

ArrayBuffer と DataView は反復可能ではありません。

20.4.3 Typed Array と通常の Array の相互変換

通常の Array を Typed Array に変換するには、それを Typed Array コンストラクタのパラメータにします。例:

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

Typed Array を Array に変換する従来の方法は、`Array.prototype.slice` を呼び出すことです。この方法は、すべての Array-like オブジェクト(`arguments` など)と Typed Array に有効です。

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

ES6 では、Typed Array は反復可能なので、スプレッド演算子(`...`)を使用できます。

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

ES6 のもう一つの選択肢は `Array.from()` であり、これは反復可能オブジェクトと Array-like オブジェクトの両方で動作します。

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

20.4.4 Typed Array の Species パターン

いくつかのメソッドは、`this` と同様の新しいインスタンスを作成します。Species パターンを使用すると、どのコンストラクタを使用するかを設定できます。例えば、`Array` のサブクラス `MyArray` を作成した場合、デフォルトでは `map()` は `MyArray` のインスタンスを作成します。`Array` のインスタンスを作成したい場合は、Species パターンを使用してそれを実現できます。詳細は、クラスに関する章の「Species パターン」を参照してください。

ArrayBuffer は以下の場所で Species パターンを使用します。

Typed Array は以下の場所で Species パターンを使用します。

DataView は Species パターンを使用しません。

20.4.5 Typed Array の継承階層

この章の冒頭の図にあるように、すべての Typed Array クラス(`Uint8Array` など)には共通のスーパー クラスがあります。そのスーパー クラスを `TypedArray` と呼んでいますが、JavaScript から直接アクセスすることはできません(ES6 仕様では、組込みオブジェクト `%TypedArray%` と呼ばれています)。`TypedArray.prototype` には、Typed Array のすべてのメソッドが含まれています。

20.4.6 静的 `TypedArray` メソッド

静的 `TypedArray` メソッドは、そのサブクラス(`Uint8Array` など)によって継承されます。

20.4.6.1 TypedArray.of()

このメソッドのシグネチャは次のとおりです。

TypedArray.of(...items)

`of()` が呼び出されたクラス(this)のインスタンスである新しい Typed Array を作成します。そのインスタンスの要素は、`of()` のパラメータです。

`of()` は Typed Array のカスタムリテラルとして考えることができます。

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

このメソッドのシグネチャは次のとおりです。

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

反復可能な `source` を `this`(Typed Array)のインスタンスに変換します。

例えば、通常の Array は反復可能であり、このメソッドで変換できます。

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

Typed Array も反復可能です。

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

オプションの `mapfn` を使用すると、結果の要素になる前に `source` の要素を変換できます。なぜ *マッピング* と *変換* の 2 つのステップを一度に行うのでしょうか? `source.map()` を使用して最初のステップを個別に実行する場合と比較して、2 つの利点があります。

  1. 中間 Array や Typed Array が不要です。
  2. 要素の精度が高い Typed Array に Typed Array を変換する場合、マッピングステップでその高い精度を利用できます。

2 番目の利点を説明するために、`map()` を使用して Typed Array の要素を 2 倍にします。

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

ご覧のように、値がオーバーフローし、`Int8` の値の範囲に強制変換されます。`from()` を使用してマップすると、値がオーバーフローしないように結果の型を選択できます。

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

Allen Wirfs-Brock 氏によると、Typed Array 間のマッピングが `from()` の `mapfn` パラメータの動機でした。

20.4.7 TypedArray.prototype のプロパティ

Typed Array メソッドで受け入れられるインデックスは負の数にすることができます(従来の Array メソッドと同様に機能します)。オフセットは非負でなければなりません。詳細は、「負のインデックス」を参照してください。

20.4.7.1 Typed Array に固有のメソッド

次のプロパティは Typed Array に固有であり、通常の Array には存在しません。

20.4.7.2 Array メソッド

次のメソッドは、基本的に通常の Array のメソッドと同じです。

これらのメソッドはすべて配列で使用できるため、動作の詳細については次の2つのソースを参照してください。

通常の配列メソッドはジェネリックである(配列のような任意のthisがOK)のに対し、このセクションにリストされているメソッドはジェネリックではありません(thisは型付き配列である必要があります)。

20.4.8 «ElementType»Arrayコンストラクタ

各型付き配列コンストラクタの名前は«ElementType»Arrayというパターンに従います。ここで«ElementType»は、冒頭の表にある要素タイプの1つです。つまり、型付き配列には9つのコンストラクタがあります。Int8ArrayUint8ArrayUint8ClampedArray(要素タイプUint8C)、Int16ArrayUint16ArrayInt32ArrayUint32ArrayFloat32ArrayFloat64Arrayです。

各コンストラクタには5つのオーバーロードされたバージョンがあります。引数の数とその型によって動作が異なります。

次のコードは、同じ型付き配列を作成する3つの異なる方法を示しています。

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 静的«ElementType»Arrayプロパティ

20.4.10 «ElementType»Array.prototypeプロパティ

20.4.11 型付き配列の連結

型付き配列には、通常の配列のようにconcat()メソッドがありません。回避策として、次のメソッドを使用します。

typedArray.set(arrayOrTypedArray, offset=0)

このメソッドは、既存の型付き配列(または通常の配列)をインデックスoffsetにあるtypedArrayにコピーします。その後、typedArrayが連結するすべての(型付き)配列を保持するのに十分な大きさであることを確認するだけです。

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 DataView

20.5.1 DataViewコンストラクタ

20.5.2 DataView.prototypeプロパティ

20.6 型付き配列をサポートするブラウザAPI

型付き配列はしばらく前から存在しているため、それをサポートするブラウザAPIがいくつかあります。

20.6.1 ファイルAPI

ファイルAPIを使用すると、ローカルファイルにアクセスできます。次のコードは、送信されたローカルファイルのバイトをArrayBufferで取得する方法を示しています。

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

新しいバージョンのXMLHttpRequest APIでは、結果をArrayBufferで配信できます。

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

XMLHttpRequestと同様に、Fetch APIを使用すると、リソースを要求できます。しかし、Promiseに基づいているため、より便利に使用できます。次のコードは、urlが指すコンテンツをArrayBufferとしてダウンロードする方法を示しています。

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

20.6.4 Canvas

HTML5仕様からの引用です。:

canvas要素は、スクリプトに解像度依存のビットマップキャンバスを提供します。これを使用して、グラフ、ゲームグラフィックス、アート、その他の視覚イメージを動的にレンダリングできます。

canvasの2Dコンテキストを使用すると、ビットマップデータをUint8ClampedArrayのインスタンスとして取得できます。

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を使用すると、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 その他のAPI

20.7 拡張例:JPEG SOF0デコーダ

この例は、JPEGファイルをアップロードしてその構造を解析し、画像の高さ、幅などを決定できるWebページです。

20.7.1 JPEGファイル形式

JPEGファイルは、セグメント(型付きデータ)のシーケンスです。各セグメントは次の4バイトで始まります。

JPEGファイルはすべてのプラットフォームでビッグエンディアンです。したがって、この例は、DataViewを使用する場合にエンディアンを指定することがいかに重要であるかを示しています。

20.7.2 JavaScriptコード

次の関数processArrayBuffer()は、実際のコードの短縮版です。混乱を避けるため、いくつかのエラーチェックを削除しました。processArrayBuffer()は、送信されたJPEGファイルの内容を含むArrayBufferを受け取り、そのセグメントを反復処理します。

// 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);
    }
}

このコードは、次のヘルパー関数(ここでは示していません)を使用します。

decodeSOF0()は、セグメント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);
}

JPEGファイルの構造に関する詳細情報

20.8 可用性

型付き配列APIの大部分は、すべての最新のJavaScriptエンジンで実装されていますが、いくつかの機能はECMAScript 6の新機能です。

これらがどこでも利用できるようになるまでには時間がかかる可能性があります。いつものように、kangaxの「ES6互換性表」は現状を説明しています。

次へ:21. イテラブルとイテレータ