目次
なぜ「バイナリデータ」とTypedArray が必要?
JavaScript はもともと「ブラウザ上のスクリプト言語」として設計されていて、
- 文字列 (
string) - 数値 (
number) - 配列 (
Array)
などの高レベルな型はあるけど、
- 「生のバイト列(8bit 配列)を決め打ちの型で解釈する」
- 「C言語の struct 的に、決められたバイトレイアウトを読む/書く」
といった処理には向いていませんでした。
しかし Web で:
- 画像・音声・動画データを直接いじる
- WebGL / WebGPU で GPU に渡す頂点バッファを組み立てる
- 独自バイナリプロトコル(WebSocket や WebRTC など)を扱う
といった需要が出てきて、高性能にバイナリを扱う仕組みとして導入されたのが
ArrayBufferTypedArray(Uint8Arrayなど)DataView
です。
ArrayBuffer:生のバイト配列(ただの「器」)
ArrayBuffer の正体
ArrayBuffer は「一定長のバイト列」を表すオブジェクトです。
中身を直接読む・書くことはできず、ビュー(TypedArray / DataView)を通して操作します。
const buffer = new ArrayBuffer(16); // 16バイトのバッファを確保
console.log(buffer.byteLength); // 16この時点では「16バイトのメモリを確保しただけ」で、中身は未定義です(一般的には 0 初期化されます)。
イメージ:
buffer (16 bytes)
[ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ]ArrayBuffer は「共有される」
複数の TypedArray が 同じ ArrayBuffer を別の見え方で参照できます。
const buffer = new ArrayBuffer(4); // 4バイト
const u8 = new Uint8Array(buffer);
const u32 = new Uint32Array(buffer);
u8[0] = 0x78;
u8[1] = 0x56;
u8[2] = 0x34;
u8[3] = 0x12;
console.log(u32[0]); // 環境のエンディアンに応じて 0x12345678 か 0x78563412- 同じ
bufferをUint8Array(1バイト単位)としても、 Uint32Array(4バイト整数)としても見られる
というのがポイントです。
TypedArray:型付き配列ビュー
代表的な TypedArray 種類
TypedArray にはいくつかの「型付きビュー」があります(全部暗記は不要、よく使うものだけ把握でOK):
- 整数系
Int8Array/Uint8Array/Uint8ClampedArrayInt16Array/Uint16ArrayInt32Array/Uint32Array
- 浮動小数系
Float32ArrayFloat64Array
よく使う例
Uint8Array:純粋なバイト列(画像のピクセル、バイナリプロトコル、etc.)Float32Array:WebGL 等で頂点バッファ(XYZ 座標など)を表すことが多い
基本的な作り方
1) 指定長で作る
const arr = new Uint8Array(4);
console.log(arr.length); // 4
console.log(arr.byteLength); // 4
console.log(arr[0]); // 02) 通常の配列からコピー
const arr = new Uint8Array([10, 20, 30]);
console.log(arr[0]); // 103) 既存の ArrayBuffer からビュー作成
const buffer = new ArrayBuffer(8);
const u8 = new Uint8Array(buffer); // 8バイト全体を1バイト単位で
const u16 = new Uint16Array(buffer); // 2バイト単位で4要素
const u8sub = new Uint8Array(buffer, 2); // オフセット2バイトからのビューコンストラクタの形:
new TypedArray(buffer, byteOffset?, length?)byteOffset:バッファ先頭からのオフセット(バイト単位)length:要素数(型のサイズ単位)
ふつうの配列っぽく使える(けど Array とは別物)
const arr = new Uint8Array([1, 2, 3]);
console.log(arr[0]); // 1
arr[1] = 255;
console.log(arr.length); // 3
console.log(arr instanceof Array); // false
console.log(Array.isArray(arr)); // false- インデックスアクセスや
lengthはArrayと同じ感覚でOK - ただし TypedArray は
Arrayのサブクラスではない(別物)
ES2015 以降、多くの Array メソッドと似たメソッドを持っています(map, forEach, filter など)
ただし完全に同じではないので、「Array とほぼ同じだけど細部が違う」くらいの認識でOK。
例:
const a = new Uint8Array([1, 2, 3, 4]);
const b = a.map(x => x * 10); // 新しい Uint8Array が返る
console.log(b); // [10, 20, 30, 40]DataView:エンディアン指定&柔軟な読み書きビュー
TypedArray は「一定の型で等間隔に並んでいる配列」を想定していますが、
- 「ヘッダ4バイトは符号なし32bit整数、次の2バイトは符号付き16bit、次は1バイトのフラグ…」のような構造化バイナリ
- リトルエンディアン / ビッグエンディアンを意識して読みたい
といった時には DataView を使います。
基本の作り方
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);コンストラクタ:
new DataView(buffer, byteOffset?, byteLength?)TypedArray とほぼ同じですが、「何バイト単位で切るか」はメソッドに依存します。
読み書きメソッド
メソッド名のパターンは:
- 読み込み:
getUint8/getInt16/getFloat32/ … - 書き込み:
setUint8/setInt16/setFloat32/ …
シグネチャ(読み取り):
view.getUint32(byteOffset, littleEndian?);byteOffset:バッファ内の読み出し位置littleEndian:true ならリトルエンディアン、false or 省略ならビッグエンディアン(仕様による)
例:ヘッダ付きバイナリの読み書き
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
// 書き込み(リトルエンディアン)
view.setUint16(0, 0x1234, true); // offset 0〜1
view.setUint32(2, 0x89abcdef, true); // offset 2〜5
// 読み込み
const header = view.getUint16(0, true);
const value = view.getUint32(2, true);
console.log(header.toString(16)); // "1234"
console.log(value.toString(16)); // "89abcdef"- 「どのバイト位置から、何バイトの値を、どのエンディアンで読むか」を自由に制御できる
- ネットワークプロトコルやバイナリファイル形式のパーサで必須
文字列 ⇔ バイト列:TextEncoder / TextDecoder
バイナリ処理では、文字列をバイト列に変換する場面も多いです(UTF-8 エンコードなど)。
TextEncoder(文字列 → Uint8Array)
const encoder = new TextEncoder(); // デフォルト UTF-8
const text = "こんにちは";
const bytes = encoder.encode(text);
console.log(bytes); // Uint8Array([...])
console.log(bytes.length); // バイト数(文字数とは異なる)- 戻り値は
Uint8Array - ブラウザ / Node.js どちらでも利用可能(比較的新しめの環境なら)
TextDecoder(Uint8Array → 文字列)
const decoder = new TextDecoder("utf-8");
const text2 = decoder.decode(bytes);
console.log(text2); // "こんにちは"- 第1引数にエンコーディング名(”utf-8″ が基本)
- 部分的なストリームデコードなどもできますが、最初は
decode一発でOK
TypedArray 実用イメージ
簡単なバイナリパケット構造を自作してみる
たとえば、次のようなバイナリ構造を送受信したいとします。
- 0〜1バイト目:パケットタイプ(
Uint16) - 2〜5バイト目:データ長(
Uint32) - 6〜(6+N-1)バイト目:UTF-8 文字列本文
パケット生成
function createPacket(type, text) {
const encoder = new TextEncoder();
const body = encoder.encode(text); // Uint8Array
const length = body.length;
const buffer = new ArrayBuffer(6 + length);
const view = new DataView(buffer);
view.setUint16(0, type, true); // type
view.setUint32(2, length, true); // length
const bodyView = new Uint8Array(buffer, 6);
bodyView.set(body); // 本文コピー
return buffer;
}パケット解析
function parsePacket(buffer) {
const view = new DataView(buffer);
const type = view.getUint16(0, true);
const length = view.getUint32(2, true);
const bodyBytes = new Uint8Array(buffer, 6, length);
const decoder = new TextDecoder("utf-8");
const text = decoder.decode(bodyBytes);
return { type, length, text };
}- 「バイト単位で仕様を決めて」「DataView / TypedArray で組み立て・分解」する、という流れをイメージできればOKです。
コメント