JavaScriptの型付き配列

2021/03/01更新

目次

基本

JavaScriptの型付き配列とは

JavaScriptにおいて、通常の配列は、型の制限無く自由な値を格納することができ、また要素数の拡張なども自由にできる。その反面、巨大なバイナリデータなどを扱おうとした場合に、処理速度の面で劣ってしまう。

そこで、より高速でデータにアクセスできるようにすることを目的に、格納できる値の型や機能に制限を設けたものが型付き配列である。通常の配列と似ている部分もあるが、似て非なるものであり、Array.isArray()による判定はfalseとなる。

型付き配列は、ECMAScript 2015(ES6)で登場した。

バッファとビュー

型付き配列は、内部的には、ArrayBufferによるメモリの確保(バッファ)と、TypedArrayによる値へのアクセス(ビュー)という2つの実装によって成り立っている。ArrayBufferを直接触ることは少なく、通常はビューであるTypedArrayのサブクラスを通して型付き配列を操作する。TypedArrayのサブクラスには以下のようなものがある。

クラス

説明

値の範囲

Int8Array

1バイト符号付き整数

-128~127

Uint8Array

1バイト整数

0~255

Uint8ClampedArray

1バイト整数(切り詰め)

0~255

Int16Array

2バイト符号付き整数

-32767~32768

Uint16Array

2バイト整数

0~65535

Int32Array

4バイト符号付き整数

-2147483648~2147483647

Uint32Array

4バイト整数

0~4294967295

Float32Array

単精度浮動小数点数(4バイト)

-3.403×10の38乗~-3.403×10の38乗

Float64Array

倍精度浮動小数点数(8バイト)

-1.798×10の308乗~1.798×10の308乗

確保されたバッファに対して、複数のビューを持つことができるので、例えば16バイトのバッファに対して、Uint8Arrayをビューとすれば1バイト×16要素の型付き配列となり、Uint16Arrayをビューとすれば2バイト×8要素の型付き配列となる。このとき、2バイト以上のビューを使用する場合は、エンディアン(バイト列の配置順序)に注意する必要がある(後述)。

Uint8ClampedArrayは、基本的にUint8Arrayと同じであるが、範囲外の値を入れられたときの挙動が異なる。Uint8Arrayでは下位ビットのみが採用されるが、Uint8ClampedArrayでは、0以下の値は全て0、255以上の値は全て255と見なされる。

ArrayBufferTypedArrayは、対応していれば必ず両方実装されているため、型付き配列を使用可能かどうかは、window.ArrayBufferが存在するかどうかだけを確認すればよい。主要なブラウザの中では、IE9以前が非対応となっている。

「Uint」の「U」は「Unsigned(符号無し)」の「U」である。

型付き配列

型付き配列の作成

型付き配列を新しく作成するには、以下のようにする。型付き配列を作成すると、内部的にArrayBufferも自動的に作成されるため、バッファを別途作成する必要はない。また、型付き配列やバッファは、一度作成するとサイズ(要素数)を変更することはできない。

// 要素数5(2バイト×5=10バイト)のUint16Arrayを作成する。
const arr1 = new Uint16Array(5);
console.log(arr1); // [0, 0, 0, 0, 0] ※初期値は0
console.log(arr1[5]); // 範囲外は「undefined」

// 通常の配列で初期データを指定して作成する。
const arr2 = new Uint16Array([254, 255, 256, 257, 258]);
console.log(arr2); // [254, 255, 256, 257, 258]

// 他の型付き配列で初期データを指定して作成する。
// 型に収まらない数値が与えられた場合は、下位ビットのみが採用される。
const arr3 = new Uint8Array(arr2);
console.log(arr3); // [254, 255, 0, 1, 2]

なお、バッファだけを作成することもできるが、これだけでは型付き配列とはならない。

// 10バイトのバッファを作成する。
const buf1 = new ArrayBuffer(10);
console.log(buf1.byteLength); // 10

// バッファの一部をコピーする。
const buf2 = buf1.slice(5, 10);

型付き配列のサイズ

型付き配列の要素数は、通常の配列と同じくlengthで分かる。また、バイト数はbyteLengthで分かる。

// Uint8Arrayは1バイトの型付き配列
const arr1 = new Uint8Array(10);
console.log(arr1.length);     // 10
console.log(arr1.byteLength); // 10

// Float64Arrayは8バイトの型付き配列
const arr2 = new Float64Array(10);
console.log(arr2.length);     // 10
console.log(arr2.byteLength); // 80

1要素あたり何バイトであるかは、BYTES_PER_ELEMENTで確認できる。

const arr = new Float64Array(10);
console.log(arr.BYTES_PER_ELEMENT);          // 8
console.log(Float64Array.BYTES_PER_ELEMENT); // 8

既存のバッファからの作成

同じバッファに対して、別の型付き配列としてアクセスすることが簡単にできる。以下の表は、10バイトのバッファに対して、いろいろな位置からいろいろな要素数で型付き配列を作成する例である。

バッファの値「0x04」「0x01」に対してUint16Arrayでの値が260となるのは、260は2バイト整数として考えると0x0104だからである。つまり、260=4×256の0乗+1×256の1乗ということである。このようにバッファ上で先に配置されているバイトを下位バイトと見なす方式をリトルエンディアンという。環境によっては、先にあるバイトが上位バイトと見なされ0x0401である1025(=4×256の1乗+1×256の0乗)となることもある。この場合はビッグエンディアンという。マルチバイトの型を扱うときにどちらのエンディアンになるかは環境次第となるため注意が必要である。また、扱うデータによっても仕様でエンディアンが決められている場合がある。

以下の表の「1バイト目から3要素のUint16Array」は作成できない。Uint16Arrayは2バイト整数であるため、必ずバッファの2の倍数の位置からしか作成することができないためである。同様に、例えばUint32Arrayであれば、4の倍数の位置からしか作成できない。

バッファ位置(バイト)

0

1

2

3

4

5

6

7

8

9

バッファ内の値

0x04

0x01

0x05

0x01

0x06

0x01

0x07

0x01

0x08

0x01

0バイト目から10要素のUint8Array

4

1

5

1

6

1

7

1

8

1

0バイト目から5要素のUint16Array

260

261

262

263

264

1バイト目から6要素のUint8Array

1

5

1

6

1

7

1バイト目から3要素のUint16Array

作成不可

2バイト目から6要素のUint8Array

5

1

6

1

7

1

2バイト目から3要素のUint16Array

261

262

263

// 要素数5(2バイト×5=10バイト)のUint16Arrayを作成する。
const arr1 = new Uint16Array([260, 261, 262, 263, 264]);
console.log(arr1); // [260, 261, 262, 263, 264]

// arr1の1要素目から3要素(=4-1)を使って別のUint16Arrayを作成する。
const arr2 = arr1.subarray(1, 4);
console.log(arr2); // [261, 262, 263]

// arr1のバッファの2バイト目から別のUint16Arrayを3要素分(2バイト×3=6バイト)作成する。
const arr3 = new Uint16Array(arr1.buffer, 2, 3);
console.log(arr3); // [261, 262, 263]

// arr1のバッファの1バイト目から別のUint8Arrayを6要素分(1バイト×6=6バイト)作成する。
const arr4 = new Uint8Array(arr1.buffer, 1, 6);
console.log(arr4); // [1, 5, 1, 6, 1, 7]

// arr1、arr2、arr3、arr4は、全てバッファを共有している。
arr3[0] = 0;
console.log(arr1); // [260, 0, 262, 263, 264]
console.log(arr2); // [0, 262, 263]
console.log(arr3); // [0, 262, 263]
console.log(arr4); // [1, 0, 0, 6, 1, 7]
console.log(arr1.buffer == arr2.buffer); // true
console.log(arr1.buffer == arr3.buffer); // true
console.log(arr1.buffer == arr4.buffer); // true

// バッファのどの部分を参照しているかは、byteOffsetで分かる。
console.log(arr1.byteOffset); // 0
console.log(arr2.byteOffset); // 2
console.log(arr3.byteOffset); // 2
console.log(arr4.byteOffset); // 1

上記arr2の例で使用しているsubarray()の第2引数は、省略すると最後の要素までとなる。

上記arr3arr4の例のように、TypedArrayのコンストラクタにバッファを与える場合、第3引数を省略するとバッファの最後までを使用して要素の作成を試みる。ただし、得られたバイト数がその型のバイトサイズの倍数に一致しない場合はエラーとなるので注意が必要である。

型付き配列の値の更新

型付き配列の値を更新するには、通常の配列と同じような代入文で1要素ずつ更新する方法の他にも、set()を使って広範囲の値をまとめて更新することができる。

const arr1 = new Uint16Array([111, 222, 333, 444, 555]);

// 先頭から[0, 0, 0]をセット
arr1.set([0, 0, 0]);
console.log(arr1); // [0, 0, 0, 444, 555];

// 2要素目から[777, 888, 999]をセット
arr1.set([777, 888, 999], 2);
console.log(arr1); // [0, 0, 777, 888, 999]);

// arr1が持つ範囲を超えてしまうので、以下はエラーとなる。
arr1.set([0, 0, 0, 0, 0, 0]);
arr1.set([0, 0, 0], 3);

通常の配列との関係

型付き配列は、通常の配列とは似て非なるものである。型付き配列を通常の配列に変換するには、Array.from()を使用すればよい。通常の配列に変換すると、型付き配列が持つアクセスの高速さは失われる。

const typedArr = new Uint16Array([111, 222, 333, 444, 555]);
const normalArr = Array.from(typedArr);

また、通常の配列が持つメソッドの大半は型付き配列でも使用できる。ただし、concat()pop()push()shift()splice()unshift()は型付き配列では使用できない。

DataView

DataViewとは

DataViewとは、バッファにアクセスするためのもう一つのビューである。TypedArrayは高速である反面、エンディアンの違いやバイト境界を考慮する必要があるなど低機能であったが、DataViewでは速度をある程度犠牲にする代わりに、エンディアンを自由に設定できたり、任意のバイト位置から値を設定できたりする機能を持っている。

DataViewの作成

DataViewを作成するには、ArrayBufferが必要である。

const buf = new ArrayBuffer(10);
const dataView1 = new DataView(buf);
const dataView2 = new DataView(buf, 0, 5); // 0バイト目から5バイト

DataViewによる値の入出力

DataViewでは、配列のような記法は使用できず、必ず以下のメソッドを使用して値を入出力する必要がある。

メソッド

意味

getInt8(n)

バッファのnバイト目から1バイト符号付き整数を読み取る。

getUint8(n)

バッファのnバイト目から1バイト整数を読み取る。

getInt16(n, e)

バッファのnバイト目から2バイト符号付き整数を読み取る。

getUint16(n, e)

バッファのnバイト目から2バイト整数を読み取る。

getInt32(n, e)

バッファのnバイト目から4バイト符号付き整数を読み取る。

getUint32(n, e)

バッファのnバイト目から4バイト整数を読み取る。

getFloat32(n, e)

バッファのnバイト目から単精度浮動小数点数を読み取る。

getFloat64(n, e)

バッファのnバイト目から倍精度浮動小数点数を読み取る。

setInt8(n, value)

バッファのnバイト目に1バイト符号付整数valueを書き込む。

setUint8(n, value)

バッファのnバイト目に1バイト整数valueを書き込む。

setInt16(n, vaule, e)

バッファのnバイト目に2バイト符号付き整数valueを書き込む。

setUint16(n, value, e)

バッファのnバイト目に2バイト整数valueを書き込む。

setInt32(n, value, e)

バッファのnバイト目に4バイト符号付き整数valueを書き込む。

setUint32(n, value, e)

バッファのnバイト目に4バイト整数valueを書き込む。

setFloat32(n, value, e)

バッファのnバイト目に単精度浮動小数点数valueを書き込む。

setFloat64(n, value, e)

バッファのnバイト目に倍精度浮動小数点数valueを書き込む。

各メソッドで指定するバイト位置nは、その型のバイトサイズの倍数である必要はない。これはTypedArrayと異なる点である。ただし、範囲外の位置や、アクセスすると範囲を超えてしまうような位置は指定できない。

上記各メソッドの引数eに真偽値を渡すことで、エンディアンを自由に指定できる。これもTypedArrayと異なる点である。trueを渡すと、リトルエンディアンで入出力され、falseを渡す、もしくは省略するとビッグエンディアンとなる。なお、1バイトの型はエンディアンを考慮する必要がないため、この引数は無い。

外部リンク