2022年7月11日

ArrayBuffer、バイナリ配列

ウェブ開発では、ファイルの処理(作成、アップロード、ダウンロード)を行う際に、主にバイナリデータに出会います。もう一つの典型的なユースケースは画像処理です。

JavaScriptではこれらすべてが可能であり、バイナリ演算は高性能です。

ただし、多くのクラスがあるため、やや混乱が生じます。いくつか例を挙げると

  • ArrayBufferUint8ArrayDataViewBlobFileなど。

JavaScriptにおけるバイナリデータは、他の言語と比較して非標準的な方法で実装されています。しかし、整理整頓すれば、すべてがかなりシンプルになります。

基本的なバイナリオブジェクトはArrayBufferです。これは、固定長の連続したメモリ領域への参照です。

次のように作成します。

let buffer = new ArrayBuffer(16); // create a buffer of length 16
alert(buffer.byteLength); // 16

これは16バイトの連続したメモリ領域を割り当て、ゼロで事前に埋め込みます。

ArrayBufferは何かの配列ではありません。

考えられる混乱の発生源を取り除きましょう。ArrayBufferArrayと共通点がありません。

  • 固定長であり、増減できません。
  • メモリにはちょうどそのだけのスペースを取ります。
  • 個々のバイトにアクセスするには、buffer[index]ではなく、別の「ビュー」オブジェクトが必要です。

ArrayBufferはメモリ領域です。そこに何が保存されているのでしょうか?それはわかりません。生のバイトシーケンスだけです。

ArrayBufferを操作するには、「ビュー」オブジェクトを使用する必要があります。

ビューオブジェクト自体は何も保存しません。これは、「眼鏡」であり、ArrayBufferに保存されているバイトの解釈を提供します。

例えば

  • Uint8ArrayArrayBuffer内の各バイトを、0から255までの値を持つ個別の数値として扱います(バイトは8ビットなので、それだけしか保持できません)。このような値は「8ビット符号なし整数」と呼ばれます。
  • Uint16Array – すべての2バイトを整数として扱い、0から65535までの値を持ちます。「16ビット符号なし整数」と呼ばれます。
  • Uint32Array – すべての4バイトを整数として扱い、0から4294967295までの値を持ちます。「32ビット符号なし整数」と呼ばれます。
  • Float64Array – すべての8バイトを浮動小数点数として扱い、5.0x10-324から1.8x10308までの値を持ちます。

したがって、16バイトのArrayBuffer内のバイナリデータは、16個の「小さな数」、または8個のより大きな数(2バイトずつ)、または4個のさらに大きな数(4バイトずつ)、または2個の高精度浮動小数点値(8バイトずつ)として解釈できます。

ArrayBufferは中心となるオブジェクトであり、すべてのもとの、生のバイナリデータです。

しかし、それに書き込んだり、それを反復処理したり、基本的にほとんどの操作を行う場合は、ビュー(例:

let buffer = new ArrayBuffer(16); // create a buffer of length 16

let view = new Uint32Array(buffer); // treat buffer as a sequence of 32-bit integers

alert(Uint32Array.BYTES_PER_ELEMENT); // 4 bytes per integer

alert(view.length); // 4, it stores that many integers
alert(view.byteLength); // 16, the size in bytes

// let's write a value
view[0] = 123456;

// iterate over values
for(let num of view) {
  alert(num); // 123456, then 0, 0, 0 (4 values total)
}

TypedArray

これらのビュー(Uint8ArrayUint32Arrayなど)の一般的な用語はTypedArrayです。これらは同じメソッドとプロパティのセットを共有しています。

TypedArrayという名前のコンストラクタはありません。これは単に、ArrayBufferに対するビューの1つを表す一般的な「包括的」な用語です。Int8ArrayUint8Arrayなど、完全なリストはすぐに続きます。

new TypedArrayのようなものを見ると、new Int8Arraynew Uint8Arrayなどのいずれかのことを意味します。

型付き配列は通常の配列のように動作します。インデックスを持ち、反復可能です。

型付き配列コンストラクタ(Int8ArrayまたはFloat64Arrayのどちらでも構いません)は、引数の型によって異なる動作をします。

引数には5つのバリエーションがあります。

new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();
  1. ArrayBuffer引数が提供された場合、その上にビューが作成されます。この構文は既に使用しています。

    オプションで、開始位置(デフォルトは0)のbyteOffsetと長さ(デフォルトではバッファの最後まで)を指定できます。その場合、ビューはbufferの一部のみをカバーします。

  2. Arrayまたは配列のようなオブジェクトが与えられた場合、同じ長さの型付き配列を作成し、内容をコピーします。

    これを使用して、データで配列を事前に埋め込むことができます。

    let arr = new Uint8Array([0, 1, 2, 3]);
    alert( arr.length ); // 4, created binary array of the same length
    alert( arr[1] ); // 1, filled with 4 bytes (unsigned 8-bit integers) with given values
  3. 別のTypedArrayが提供された場合、同じことを行います。同じ長さの型付き配列を作成し、値をコピーします。必要に応じて、その過程で値が新しい型に変換されます。

    let arr16 = new Uint16Array([1, 1000]);
    let arr8 = new Uint8Array(arr16);
    alert( arr8[0] ); // 1
    alert( arr8[1] ); // 232, tried to copy 1000, but can't fit 1000 into 8 bits (explanations below)
  4. 数値引数lengthの場合、その多くの要素を含む型付き配列を作成します。そのバイト長は、単一アイテムTypedArray.BYTES_PER_ELEMENTのバイト数にlengthを掛けたものになります。

    let arr = new Uint16Array(4); // create typed array for 4 integers
    alert( Uint16Array.BYTES_PER_ELEMENT ); // 2 bytes per integer
    alert( arr.byteLength ); // 8 (size in bytes)
  5. 引数がない場合、長さ0の型付き配列を作成します。

ArrayBufferに言及せずに、TypedArrayを直接作成できます。しかし、ビューは基となるArrayBufferがなければ存在できないため、最初のもの(提供された場合)を除くすべての場合で自動的に作成されます。

基となるArrayBufferにアクセスするには、TypedArrayに次のプロパティがあります。

  • bufferArrayBufferを参照します。
  • byteLengthArrayBufferの長さ。

したがって、常に1つのビューから別のビューに移動できます。

let arr8 = new Uint8Array([0, 1, 2, 3]);

// another view on the same data
let arr16 = new Uint16Array(arr8.buffer);

型付き配列のリストを以下に示します。

  • Uint8ArrayUint16ArrayUint32Array – 8、16、32ビットの整数の数。
    • Uint8ClampedArray – 8ビット整数の場合、代入時に「クランプ」します(下記参照)。
  • Int8ArrayInt16ArrayInt32Array – 符号付き整数(負になる可能性があります)。
  • Float32ArrayFloat64Array – 32ビットと64ビットの符号付き浮動小数点数。
int8または同様の単一値型はありません。

Int8Arrayのような名前にもかかわらず、JavaScriptにはintint8のような単一値型はありません。

これは論理的です。Int8Arrayはこれらの個々の値の配列ではなく、ArrayBufferのビューだからです。

範囲外の動作

型付き配列に範囲外の値を書こうとするとどうなるでしょうか?エラーは発生しません。しかし、余分なビットは切り捨てられます。

例えば、256をUint8Arrayに入れようとします。2進数形式で、256は100000000(9ビット)ですが、Uint8Arrayは値ごとに8ビットしか提供しないため、利用可能な範囲は0から255になります。

より大きな数値の場合、最右端(最下位)の8ビットのみが保存され、残りは切り捨てられます。

したがって、ゼロになります。

257の場合、2進数形式は100000001(9ビット)であり、最右端の8ビットが保存されるため、配列には1が入ります。

言い換えれば、28を法とする数が保存されます。

デモはこちら

let uint8array = new Uint8Array(16);

let num = 256;
alert(num.toString(2)); // 100000000 (binary representation)

uint8array[0] = 256;
uint8array[1] = 257;

alert(uint8array[0]); // 0
alert(uint8array[1]); // 1

Uint8ClampedArrayはこの点で特殊であり、動作が異なります。255より大きい数値には255を、負の数には0を保存します。この動作は画像処理に役立ちます。

TypedArrayメソッド

TypedArrayには、注目すべき例外を除いて、通常のArrayメソッドがあります。

反復、mapslicefindreduceなどを実行できます。

ただし、できないことがいくつかあります。

  • spliceはありません – 型付き配列はバッファのビューであり、それらは固定された連続したメモリ領域であるため、値を「削除」することはできません。できることはゼロを代入することだけです。
  • concatメソッドはありません。

2つの追加メソッドがあります。

  • arr.set(fromArr, [offset])は、fromArrのすべての要素をarrにコピーし、位置offset(デフォルトは0)から開始します。
  • arr.subarray([begin, end])は、beginからend(排他的)まで、同じ型の新しいビューを作成します。これはsliceメソッド(これもサポートされています)に似ていますが、何もコピーしません。データの指定された部分に対して操作を行う新しいビューを作成するだけです。

これらのメソッドを使用すると、型付き配列をコピーしたり、混合したり、既存の配列から新しい配列を作成したりすることができます。

DataView

DataViewは、ArrayBufferに対する特別な超柔軟な「型なし」ビューです。これにより、任意のフォーマットで任意のオフセットのデータにアクセスできます。

  • 型付き配列の場合、コンストラクタによってフォーマットが決まります。配列全体は均一である必要があります。i番目の数はarr[i]です。
  • DataViewでは、.getUint8(i)または.getUint16(i)などのメソッドを使用してデータにアクセスします。構築時ではなく、メソッド呼び出し時にフォーマットを選択します。

構文

new DataView(buffer, [byteOffset], [byteLength])
  • buffer – 基となるArrayBuffer。型付き配列とは異なり、DataViewは独自にバッファを作成しません。準備しておく必要があります。
  • byteOffset – ビューの開始バイト位置(デフォルトは0)。
  • byteLength – ビューのバイト長(デフォルトはbufferの最後まで)。

例えば、ここでは同じバッファから異なるフォーマットの数値を抽出します。

// binary array of 4 bytes, all have the maximal value 255
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;

let dataView = new DataView(buffer);

// get 8-bit number at offset 0
alert( dataView.getUint8(0) ); // 255

// now get 16-bit number at offset 0, it consists of 2 bytes, together interpreted as 65535
alert( dataView.getUint16(0) ); // 65535 (biggest 16-bit unsigned int)

// get 32-bit number at offset 0
alert( dataView.getUint32(0) ); // 4294967295 (biggest 32-bit unsigned int)

dataView.setUint32(0, 0); // set 4-byte number to zero, thus setting all bytes to 0

DataViewは、同じバッファに混合フォーマットのデータを保存する場合に最適です。例えば、(16ビット整数、32ビット浮動小数点数)のペアのシーケンスを保存する場合、DataViewを使用すると簡単にアクセスできます。

概要

ArrayBufferは中心となるオブジェクトであり、固定長の連続したメモリ領域への参照です。

ArrayBufferに対してほとんどの操作を行うには、ビューが必要です。

  • それはTypedArrayである可能性があります。
    • Uint8ArrayUint16ArrayUint32Array – 8、16、32ビットの符号なし整数。
    • Uint8ClampedArray – 8ビット整数の場合、代入時に「クランプ」します。
    • Int8ArrayInt16ArrayInt32Array – 符号付き整数(負になる可能性があります)。
    • Float32ArrayFloat64Array – 32ビットと64ビットの符号付き浮動小数点数。
  • またはDataView – フォーマットを指定するためにメソッドを使用するビュー(例:getUint8(offset))。

ほとんどの場合、ArrayBufferを「共通の分母」として隠したまま、型付き配列を直接作成して操作します。「共通の分母」として隠しておきます。必要に応じて、.bufferとしてアクセスし、別のビューを作成できます。

バイナリデータに対して動作するメソッドの説明で使用される追加の用語が2つあります。

  • ArrayBufferViewは、これらのすべての種類のビューの包括的な用語です。
  • BufferSourceは、ArrayBufferまたはArrayBufferViewの包括的な用語です。

これらの用語は次の章で説明します。BufferSourceは最も一般的な用語の1つであり、「あらゆる種類のバイナリデータ」つまりArrayBufferまたはそのビューを意味します。

チートシートはこちら

課題

Uint8Arrayの配列が与えられた場合、それらを単一の配列に連結する関数concat(arrays)を作成します。

テストを含むサンドボックスを開きます。

function concat(arrays) {
  // sum of individual array lengths
  let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);

  let result = new Uint8Array(totalLength);

  if (!arrays.length) return result;

  // for each array - copy it over result
  // next array is copied right after the previous one
  let length = 0;
  for(let array of arrays) {
    result.set(array, length);
    length += array.length;
  }

  return result;
}

テストを含むソリューションをサンドボックスで開きます。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点の提案がある場合は、コメントする代わりに、GitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数語のコードを挿入するには<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合は、サンドボックス(plnkrjsbincodepen…)を使用してください。