JavaScript for impatient programmers (ES2022 edition)
この本のサポートをお願いします:購入または寄付
(広告です。ブロックしないでください。)

32 型付き配列:バイナリデータの処理(上級)



32.1 APIの基本

Web上の多くのデータはテキストです。JSONファイル、HTMLファイル、CSSファイル、JavaScriptコードなどです。JavaScriptは、組み込みの文字列を介してそのようなデータをうまく処理します。

しかし、2011年以前は、バイナリデータをうまく処理できませんでした。Typed Array Specification 1.0は2011年2月8日に導入され、バイナリデータを扱うためのツールを提供しています。ECMAScript 6では、型付き配列がコア言語に追加され、以前は通常の配列(.map().filter()など)でのみ使用可能だったメソッドが追加されました。

32.1.1 型付き配列のユースケース

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

32.1.2 コアクラス:ArrayBuffer、型付き配列、DataView

型付き配列APIは、バイナリデータをArrayBufferのインスタンスに格納します

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

ArrayBuffer自体はブラックボックスです。そのデータにアクセスするには、別のオブジェクト、つまり*ビューオブジェクト*でラップする必要があります。2種類のビューオブジェクトを使用できます

20は、APIのクラス図を示しています。

Figure 20: The classes of the Typed Array API.

32.1.3 型付き配列の使用

型付き配列は、いくつかの注目すべき違いを除いて、通常の配列とほぼ同じように使用されます

32.1.3.1 型付き配列の作成

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

// 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 ラップされた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 要素の取得と設定
const typedArray = new Int16Array(2);

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

32.1.4 DataViewの使用

DataViewの使い方は次のとおりです

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 要素の型

表20:型付き配列APIでサポートされている要素の型。
要素 型付き配列 バイト数 説明
Int8 Int8Array 1 1 8ビット符号付き整数
ES6 Uint8 1 Uint8Array 8ビット符号付き整数
1 8ビット符号なし整数 1 Uint8Array 8ビット符号付き整数
Uint8C 8ビット符号付き整数
Uint8ClampedArray 1 2 (クランプ変換) 8ビット符号付き整数
Int16 Int16Array 2 2 8ビット符号付き整数
16ビット符号付き整数 Uint16 4 Uint16Array 8ビット符号付き整数
2 16ビット符号なし整数 4 Int32 8ビット符号付き整数
Int32Array 4 8 32ビット符号付き整数 Uint32
Uint32Array 4 8 32ビット符号なし整数 Uint32
BigInt64 BigInt64Array 4 8 8ビット符号付き整数
64ビット符号付き整数 ES2020 8 BigUint64 8ビット符号付き整数

BigUint64Array

Float32

Float32Array

Float64

Float64Array

20は、使用可能な要素の型をリストしています。これらの型(例:Int32)は、2つの場所に表示されます

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

型付き配列では、要素の型を指定します。たとえば、Int32Arrayのすべての要素は、型Int32を持ちます。要素の型は、型付き配列で異なる唯一の側面です。

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

DataViewでは、.getInt32().setInt32()などのメソッドを使用するときに、ArrayBufferにアクセスするためのレンズとなります。

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

要素型Uint8Cは特殊です。DataViewではサポートされておらず、Uint8ClampedArrayを有効にするためにのみ存在します。この型付き配列は、canvas要素(CanvasPixelArrayを置き換える場所)で使用され、それ以外の場合は避ける必要があります。Uint8CUint8の唯一の違いは、オーバーフローとアンダーフローの処理方法です(次のサブセクションで説明)。

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.1 オーバーフローとアンダーフローの処理

最小値から1を引くと、最大値に変換されます。

次の関数は、変換の仕組みを説明するのに役立ちます

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

アンダーフローするすべての値は、最小値に変換されます。

オーバーフローするすべての値は、最大値に変換されます。

32.2.2 エンディアン

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

ビッグエンディアン:最上位バイトが最初に来ます。たとえば、Uint16値0x4321は、2つのバイト(最初に0x43、次に0x21)として格納されます。

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

リトルエンディアン:最下位バイトが最初に来ます。たとえば、Uint16値0x4321は、2つのバイト(最初に0x21、次に0x43)として格納されます。

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

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

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

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

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

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

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

  1. 中間 Array または Typed Array は必要ありません。
  2. 異なる精度の Typed Array 間で変換する場合、エラーが発生する可能性が低くなります。

2 番目の利点の説明については、以下をお読みください。

32.3.1.1 落とし穴: Typed Array タイプ間で変換しながらマッピングする

静的メソッド .from() は、オプションで Typed Array タイプ間のマッピングと変換の両方を行うことができます。 このメソッドを使用すると、エラーが発生する可能性が低くなります。

その理由を理解するために、まず Typed Array をより高い精度の Typed Array に変換してみましょう。 .from() を使用してマッピングする場合、結果は自動的に正しくなります。 そうでない場合は、最初に変換してからマッピングする必要があります。

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

Typed Array から精度が低い Typed Array に変換する場合、.from() を介したマッピングは正しい結果を生成します。 そうでない場合は、最初にマッピングしてから変換する必要があります。

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

問題は、.map() を介してマッピングする場合、入力タイプと出力タイプが同じになることです。 対照的に、.from() は、任意の入力タイプから、レシーバーを介して指定する出力タイプに変換します。

32.3.2 Typed Array は反復可能です

Typed Array は反復可能です。 つまり、for-of ループやその他の反復ベースのメカニズムを使用できます。

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

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

32.3.3 Typed Array と通常の Array の比較

Typed Array は通常の Array と非常によく似ています。.length があり、要素にはブラケット演算子 [] を介してアクセスでき、標準の Array メソッドのほとんどを備えています。 Typed Array は、以下の点で通常の Array とは異なります。

32.3.4 Typed Array と通常の Array の間の変換

通常の Array を Typed Array に変換するには、Typed Array コンストラクター(Array ライクなオブジェクトと Typed Array を受け入れる)または «ElementType»Array.from()(反復可能オブジェクトと Array ライクなオブジェクトを受け入れる)に渡します。 例:

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

Typed Array を通常の Array に変換するには、Array.from() またはスプレッド構文を使用できます(Typed Array は反復可能であるため)。

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 Typed Array の連結

Typed Array には、通常の Array のような .concat() メソッドがありません。 回避策は、オーバーロードされたメソッド .set() を使用することです。

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

既存の typedArray または arrayLike を、インデックス offset のレシーバーにコピーします。 TypedArray は、すべての具体的な Typed Array クラスの架空の抽象スーパークラスです。

次の関数は、そのメソッドを使用して、ゼロ個以上の Typed Array(または Array ライクなオブジェクト)を resultConstructor のインスタンスにコピーします。

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 クイックリファレンス: インデックスとオフセット

ArrayBuffer、Typed Array、および DataView のクイックリファレンスの準備として、インデックスとオフセットの違いを学ぶ必要があります。

パラメータがインデックスかオフセットかは、ドキュメントを確認することによってのみ判別できます。簡単なルールはありません。

32.5 クイックリファレンス: ArrayBuffer

ArrayBuffer はバイナリデータを格納し、Typed Array および DataView を介してアクセスすることを目的としています。

32.5.1 new ArrayBuffer()

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

new ArrayBuffer(length: number)

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

ArrayBuffer の長さを変更することはできません。 長さの異なる新しいものを作成することしかできません。

32.5.2 ArrayBuffer の静的メソッド

32.5.3 ArrayBuffer.prototype のプロパティ

32.6 クイックリファレンス: Typed Array

さまざまな Typed Array オブジェクトのプロパティは、2 つのステップで導入されます。

  1. TypedArray: まず、すべての Typed Array クラスの抽象スーパークラス(この章の冒頭のクラス図で示されています)を見ていきます。 このスーパークラスを TypedArray と呼んでいますが、JavaScript から直接アクセスすることはできません。 TypedArray.prototype は、Typed Array のすべてのメソッドを格納します。
  2. «ElementType»Array: 具体的な Typed Array クラスは、Uint8ArrayInt16ArrayFloat32Array などと呼ばれます。 これらは、new.of、および .from() を介して使用するクラスです。

32.6.1 TypedArray<T> の静的メソッド

両方の静的 TypedArray メソッドは、そのサブクラス(Uint8Array など)によって継承されます。 TypedArray は抽象的です。 したがって、これらのメソッドは常に、具体的で直接インスタンスを持つことができるサブクラスを介して使用します。

32.6.2 TypedArray<T>.prototype のプロパティ

Typed Array メソッドで受け入れられるインデックスは負にすることができます(従来の Array メソッドと同じように機能します)。 オフセットは非負である必要があります。 詳細については、§32.4「クイックリファレンス: インデックスとオフセット」を参照してください。

32.6.2.1 Typed Array 固有のプロパティ

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

32.6.2.2 Array メソッド

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

これらのメソッドの動作の詳細については、§31.13.3 “Array.prototypeのメソッド”を参照してください。

32.6.3 new «ElementType»Array()

各 Typed Array コンストラクタは、«ElementType»Array のパターンに従う名前を持ちます。ここで、«ElementType» は冒頭の表にある要素型のいずれかです。つまり、Typed Array には 11 個のコンストラクタがあります。

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

32.6.4 «ElementType»Array の静的プロパティ

32.6.5 «ElementType»Array.prototype のプロパティ

32.7 クイックリファレンス: DataView

32.7.1 new DataView()

32.7.2 DataView.prototype のプロパティ

このセクションの残りの部分では、«ElementType» は次のいずれかを指します。

これらは DataView.prototype のプロパティです。