前回からのつづき
前回、オブジェクト型の基本である Object オブジェクトに始まり、非同期処理で使用する Promise オブジェクトについて説明してきました。今回は、引き続き、オブジェクト型のバイナリデータから説明していきたいと思います。
🧊 バイナリデータ(Binary Data)
JavaScript でバイナリデータを扱うときに登場するのが ArrayBuffer / TypedArray / DataView / Blob / File / Stream といった オブジェクトAPI です。
これらは OPFS(Origin Private File System)や IndexedDB 、 WebAssembly 、画像処理、ネットワーク通信など、実務で必須の基礎になります。
全体の関係図
各バイナリオブジェクト群の構成を下記に示します。
ArrayBuffer ← 生メモリ
├─ TypedArray(型付きビュー)
│ ├─ Uint8Array
│ ├─ Int8Array
│ ├─ Uint8ClampedArray
│ ├─ Int16Array
│ ├─ Uint16Array
│ ├─ Int32Array
│ ├─ Uint32Array
│ ├─ Float32Array
│ ├─ Float64Array
│ ├─ BigInt64Array
│ └─ BigUint64Array
└─ DataView(柔軟ビュー)
SharedArrayBuffer ← 共有メモリ
├─ TypedArray(型付きビュー)
│ ├─ Uint8Array
│ ├─ Int8Array
│ ├─ Uint8ClampedArray
│ ├─ Int16Array
│ ├─ Uint16Array
│ ├─ Int32Array
│ ├─ Uint32Array
│ ├─ Float32Array
│ ├─ Float64Array
│ ├─ BigInt64Array
│ └─ BigUint64Array
└─ DataView(柔軟ビュー)
Blob(バイナリの塊)
└─ File(ユーザーのファイル)
Stream(分割読み書き)
TypedArray / DataView は単独で存在するオブジェクトではなく、ArrayBuffer / SharedArrayBuffer オブジェクトと密接に結びついています。
ArrayBuffer — 生メモリ領域
ArrayBuffer は「固定長の生メモリ」オブジェクトです。中身はただのバイト列で、型情報は一切ありません。
const buffer = new ArrayBuffer(1024); // 1024 バイトのメモリ
作成されたサイズは固定され、後から伸ばすことはできません。
中身は 0 で初期化されます。
「ただの箱」なので、このオブジェクト自身に、直接読み書きはできません。読み書きには、後述の TypedArray / DataView オブジェクトが必要です。
ArrayBuffer オブジェクトの読み書きは、TypedArray / DataView オブジェクトのメソッドを通して行います。
ArrayBuffer オブジェクトは 実バイナリデータを保持する為の領域であり、TypedArray / DataView オブジェクトの土台になるものです。
SharedArrayBuffer — 共有メモリ領域
SharedArrayBuffer は、ArrayBuffer と同様に固定長のバイト列を保持するオブジェクトです。
ArrayBuffer オブジェクトが単一の実行コンテキストで利用することを前提としているのに対し、SharedArrayBuffer オブジェクトは複数の実行コンテキスト(Main スレッドや Web Worker など)から同じメモリ領域を共有できる点が異なります。
const buffer = new SharedArrayBuffer(1024);
const bytes = new Uint8Array(buffer);
TypedArray オブジェクトや DataView オブジェクトを利用して読み書きする点は ArrayBuffer オブジェクトと同じです。
SharedArrayBuffer オブジェクトは、主に高速なデータ共有や並列処理で利用されます。詳しくは Web Worker や Atomics を説明する記事、、「モダンJavaScript入門/Web Worker の使い方③」以降で解説します。
TypedArray — 型付き配列(高速アクセス)
TypedArray は ArrayBuffer / SharedArrayBuffer オブジェクトを「特定の型で読み書きするためのビュー」オブジェクトです。
┌──┬──┬──┬──┐
│FF│80│00│00│ ArrayBuffer (生メモリ) / SharedArrayBuffer (共有メモリ)
└──┴──┴──┴──┘
▲
│
▼
┌──┬──┬──┬──┐
│FF│80│00│00│ TypedArray — 型付きビュー
└──┴──┴──┴──┘
TypedArray オブジェクトでは、ArrayBuffer オブジェクトの実メモリ領域の生メモリや、SharedArrayBuffer オブジェクトの共有メモリを特定の型で読み書きできます。
// ArrayBuffer(生メモリ)
const buffer = new ArrayBuffer(1024);
const bytes = new Uint8Array(buffer);
bytes[0] = 255;
bytes[1] = 128;
// SharedArrayBuffer(共有メモリ)
const buffer_s = new SharedArrayBuffer(1024);
const bytes_s = new Uint8Array(buffer_s);
bytes_s[0] = 255;
bytes_s[1] = 128;
TypedArray オブジェクトには、下記の型が存在します。
Uint8Array(0〜255 の整数)Int8ArrayUint8ClampedArrayInt16ArrayUint16ArrayInt32ArrayUint32ArrayFloat32ArrayFloat64ArrayBigInt64ArrayBigUint64Array
特徴
- 高速(ネイティブに近い)
- 型が固定(Uint8Array なら 0〜255)
- 配列のように扱える
- 画像処理、音声処理、暗号化、WebAssembly などで必須。
ArrayBufferを明示しない方法
ArrayBufferを明示しない定義も可能です。( SharedArrayBuffer は必ず明示が必要です。)
const bytes = new Uint8Array([255, 128, 0, 0]);
この場合、暗黙で ArrayBuffer が作成されます。bytes.buffer でアクセス可能です。
※ 暗黙で生成される ArrayBuffer のサイズは、配列の要素数に応じて自動的に決まります。(Uint8Array なら 1 要素 = 1 バイト)
DataView — 任意の型で読み書きできる柔軟ビュー
TypedArray オブジェクトは「型が固定」ですが、DataView は 任意の型を自由に読み書きできるオブジェクトです。
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│40│E2│01│00│18│2D│44│54│FB│21│09│40│00│00│00│00│ ArrayBuffer (生メモリ) /
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ SharedArrayBuffer (共有メモリ)
└──┴──┴──┴──┘ DataView — 柔軟ビュー
▲ └──┴──┴──┴──┴──┴──┴──┴──┘
│ ▲
▼ │
┌──┬──┬──┬──┐ │
│40│E2│01│00│ uint32 │
└──┴──┴──┴──┘ ▼
┌──┬──┬──┬──┬──┬──┬──┬──┐
│18│2D│44│54│FB│21│09│40│ Float64
└──┴──┴──┴──┴──┴──┴──┴──┘
領域を部分的に、任意の型で読み書き可能です。
// ArrayBuffer(生メモリ)
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
view.setUint32(0, 123456); //0x0001E240
view.setFloat64(4, Math.PI); // 0x400921FB54442D18
console.log(view.getUint32(0)); // 123456
console.log(view.getFloat64(4)); // 3.141592653589793
// SharedArrayBuffer(共有メモリ)
const buffer_s = new SharedArrayBuffer(16);
const view_s = new DataView(buffer_s);
view_s.setUint32(0, 123456); //0x0001E240
view_s.setFloat64(4, Math.PI); // 0x400921FB54442D18
console.log(view_s.getUint32(0)); // 123456
console.log(view_s.getFloat64(4)); // 3.141592653589793
特徴
getUint32(offset, littleEndian)のようにエンディアン(byte order)を指定できる- 複数の型を混在できる
- バイナリフォーマット(PNG, WAV, ZIP など)解析に必須
- 実務で「バイナリを読む」なら DataView は避けて通れません。
エンディアン(byte order)とは
複数バイトで構成される数値を、メモリ上でどの順番で並べるか というルールのことです。
1 バイトなら順番は関係ありませんが、2 バイト(Uint16)、4 バイト(Uint32)、8 バイト(Float64)になると並び順の違いが問題になります。
エンディアンには、並べる順番に応じて、ビッグエンディアン(Big Endian)と リトルエンディアン(Little Endian)の 2 種類が存在します。
① ビッグエンディアン(Big Endian)
上位バイト → 下位バイト の順に並べる方式。
0x12345678 を Big Endian で並べると:
12 34 56 78
👉 人間が 16進数を書く順番と同じなので 直感的。
② リトルエンディアン(Little Endian)
下位バイト → 上位バイト の順に並べる方式。
0x12345678 を Little Endian で並べると:
78 56 34 12
👉 Intel CPU や JavaScript の TypedArray が採用している方式。
TypedArray は、実行環境のネイティブエンディアンでデータを読み書きします。現在の主要なブラウザが動作する環境では、ほとんどが Little Endian です。エンディアンを明示的に指定したい場合は、DataView を使用します。
const f64 = new Float64Array([Math.PI]);
const u8 = new Uint8Array(f64.buffer);
console.log([...u8].map(x => x.toString(16).padStart(2, "0")).join(" "));
出力例:
18 2d 44 54 fb 21 09 40
これは リトルエンディアンなので、実際の Float64 のビッグエンディアン表現(IEEE 754)は逆順になります:
40 09 21 fb 54 44 2d 18
この様にエンディアンが異なると型が同じでも、全く異なる値になってしまいます。
※ ネットワークでは Big Endian が多いです。また、ファイルも形式によっては Big Endian のものも存在する為、注意が必要です。
どのような場合に、エンディアンが重要なのか(実務ポイント)
- ArrayBuffer / SharedArrayBuffer / DataView でバイナリを読むとき
- PNG / WAV / ZIP などのファイルフォーマット解析
- ネットワーク通信(プロトコルは Big Endian が多い)
- OPFS にバイナリを書き込むとき
- WebAssembly のメモリ操作
👉 エンディアンを間違えると値が壊れます。
Blob — バイナリデータの塊(ファイルのように扱える)
Blob は 複数のデータ片(文字列・ArrayBuffer・TypedArray)をひとつの“かたまり”にまとめるオブジェクトです。
Blob は 作成時に、配列で複数のデータ片を渡すと、それらを連結して 1 つの塊にします。
1. 複数の文字列をまとめて 1 つの Blob にする
const blob = new Blob([
"Hello, ",
"Kitsune-san!",
"\nThis is a blob chunk."
]);
console.log(blob.size); // 36 など
2. バイナリ(Uint8Array)を複数まとめる
const header = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
const body = new Uint8Array([0x10, 0x20, 0x30, 0x40]);
const blob = new Blob([header, body]);
console.log(blob.size); // 8
👉 これは ファイルフォーマットの構築に近いイメージです。PNG や ZIP のような形式は、まさにこういう「複数のバイナリ片の連結」でできています。
3. JSON を Blob にして「ファイル化」する
const data = { name: "Kitsune", age: 20 };
const json = JSON.stringify(data);
const blob = new Blob([json], { type: "application/json" });
👉 Blob は ファイルのように扱えるので、この blob をダウンロードさせたり、OPFS に保存したりできます。
4. 画像のバイナリを Blob にまとめる(実務でよく使う)
const response = await fetch("/images/fox.png");
const arrayBuffer = await response.arrayBuffer();
const blob = new Blob([arrayBuffer], { type: "image/png" });
👉 fetch → ArrayBuffer → Blob という流れは 画像処理・OPFS 保存・IndexedDB 保存で頻出します。
5. 音声データのチャンク(小分け)をまとめる(ストリームっぽい例)
const chunk1 = new Uint8Array([/* ... */]);
const chunk2 = new Uint8Array([/* ... */]);
const chunk3 = new Uint8Array([/* ... */]);
const blob = new Blob([chunk1, chunk2, chunk3], { type: "audio/wav" });
👉 音声や動画は 複数チャンクをまとめて 1 つの Blob にすることが多いです。
6. テキスト + バイナリを混ぜて 1 Blob にする
const header = "FILE_HEADER\n";
const binary = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
const blob = new Blob([header, binary]);
👉 Blob は 文字列とバイナリを混ぜても OK なので、独自ファイル形式を作るときに便利です。
特徴
- サイズは可変。
- 中身は ArrayBuffer / TypedArray / 文字列などファイルのように扱えます。
URL.createObjectURL()でブラウザ表示も可能- OPFS や fetch API と相性が良い。
File — ユーザーが選択したファイル
File は Blob を継承した「実際のファイル」を示すオブジェクトです。
const file = input.files[0];
特徴
- Blob の機能をすべて持つ
- 追加で name / lastModified / type を持つ
<input type="file">やドラッグ&ドロップで得られる- OPFS に保存する場合も、File → ArrayBuffer の変換が必要。
Stream — 大きなデータを分割して扱う
巨大ファイルを一度に読み込むとメモリを圧迫します。そこで登場するのが Stream オブジェクトです。
Stream オブジェクトは、巨大なデータを 一度に全部扱うのではなく、小さなチャンク(かたまり)に分けて流すための仕組みです。
Stream オブジェクトには、
- ReadableStream:データを「読み出す側」
- WritableStream:データを「書き込む側」
の 2 種類のオブジェクトが存在します。
ReadableStream オブジェクトと WritableStream オブジェクトは 対になる存在です。
ReadableStream(読み出し)
ReadableStream は、読み出し専用のストリームです。
データをストリームとして読み込みます。
const blob = new Blob(["Hello Kitsune!"]);
const stream = blob.stream(); // ReadableStream
※ blob.stream() で得られるのは ReadableStream です。
Blob は「バイナリのかたまり」なので、これをストリームとして読み出すことができます。
ReadableStream は内部的に次のように動きます:
- Blob の中身を 小さなチャンクに分割
- 必要になったタイミングで 順番に取り出す
- メモリを圧迫しない
👉 ファイルなども、File オブジェクトを介して、この方法で読み込むことが可能です。
WritableStream(書き込み)
WritableStream は、書き込み専用のストリームです。
データをストリームとして、書き出します。
const handle = await fileHandle.createWritable(); // WritableStream
await handle.write(chunk); // チャンクを書き込む
await handle.close();
※ OPFS のファイル書き込みは WritableStream を使います。
WritableStream は:
- チャンクを 順番に書き込む
- 大きなファイルでも メモリを使いすぎない
- 書き込み途中でストリームを閉じることもできる
特徴:
- データを「チャンク(小分け)」で処理できる
- メモリ効率が良い
fetch()や OPFS の FileSystemWritableFileStream と連携- OPFS の書き込みは ストリーム API が基本になります。
便利な使い方
ReadableStream と WritableStream は pipeTo() で接続できます。
const readable = blob.stream(); // ReadableStream
const writable = await fileHandle.createWritable(); // WritableStream
await readable.pipeTo(writable);
データのかたまりである Blob から ReadableStream を取得し、pipeTo() により、WritableStream を接続します。
これにより、ReadableStream オブジェクトは、Blob を小分けにして流す。WritableStream オブジェクトは流れてきた小分けを受け取って書くという処理が行われます。
👉 この方式を利用すると、巨大ファイルでも効率よく処理できます。
まとめ
この記事で扱った内容は、OPFS のようなブラウザ API を扱う際に必須の基礎です。
- プリミティブ型は、値そのもの
- Symbol は、一意の識別子 を作るための型(ES6 で追加されたプリミティブ型)
- BigIntは、umber で扱えない非常に大きな整数を扱うための型(ES6 以降で追加されたプリミティブ型)
- オブジェクト型は、参照
- Map / Set は、モダンなデータ構造のオブジェクト(ES6 で追加されたオブジェクト型)
- Promise は、非同期処理の中心となるオブジェクト(ES6 で追加されたオブジェクト型)
- ArrayBuffer は、生メモリ
- SharedArrayBuffer は、共有メモリ
- TypedArray は、高速で型付きのビュー
- TypedArray は、実行環境のネイティブエンディアンで読み書きする(現在の多くの環境では Little Endian)
- ネットワークでは、仕様で規定されていることもあり、 Big Endian が多い
- DataView は、任意の型で読み書きできる柔軟ビュー
- getUint32(offset, littleEndian) のようにエンディアンを指定できるので実務で便利
- Blob は、バイナリの塊(ファイルのように扱える)
- File は、ユーザーが選択した実ファイル
- Stream は、巨大データを分割して扱う仕組み