はじめに
バイナリデータを扱うときにちょこちょこ出てくる、Buffer, Blob, Base64, ...
出てくるたびに少し調べて、わかった気になって、忘れたころにまたやってくる...
お前ら一体なんなんだよぉと思ったので、ちゃんとアウトプットしてみた。
そもそもバイナリって?
IT用語辞典によれば、以下とのこと。
バイナリ(binary)とは、一般には、二進数もしくは0と1で表現される数字のこと。
しかし、曖昧な使われ方をしているようで、人間に向けではないコンピュータ用のデータを総じてバイナリデータといったりするみたい。
型付き配列
JavaScriptの世界で、通常の配列は色々な値を入れられるし、拡張なども自由で非常に便利。
しかし、巨大なバイナリデータなどを扱うときは処理速度遅い。。。
→ 型付き配列というものを使おう!!!
- 通常の配列:型の制限無く自由な値を格納可能。要素数の拡張なども自由。
- 型付き配列:格納する値の型や機能に制限あり。→ 高速データアクセス
この型付き配列を扱う時に、バッファーとビューというものを使う!!
役割 | 実装 | |
---|---|---|
バッファー | メモリの確保 | ArrayBufferクラス |
ビュー | 値へのアクセス | TypedArrayオブジェクト、DataView |
※DataViewはここでは割愛します。
ArrayBufferクラス
文字通り、バイトの配列。以下のように書けば、16バイトの配列。
const buffer = new ArrayBuffer(16);
ArrayBuffer の内容(確保したメモリ)を直接操作することはできない。
なら、どうすんねん。→ ビューを使う!!!
TypedArrayオブジェクト
TypedArrayコンストラクタがあるわけではない!!
実際にあるのは、以下のようなサブクラス(ほかにもいろいろある)。
型 | 値の範囲 | バイト数 |
---|---|---|
Int8Array | -128 ~ 127 | 1 |
Uint8Array | 0 ~ 255 | 1 |
Int16Array | -32768 ~ 32767 | 2 |
Uint16Array | 0 ~ 65535 | 2 |
Int8: 8bit(1バイト)の整数型。
Uint = Unsigned int なので、符号なし整数型のこと。
ArrayBufferでメモリを確保し、確保したメモリに対して、Uint8Arrayでビューを作成しアクセスしてみる。
const buffer = new ArrayBuffer(16); // 16バイト
const uint8Array = new Uint8Array(buffer, 0, 3) // 第2引数: byteOffset, 第3引数: length
console.log(uint8Array)
// Uint8Array(16) [0, 0, 0]
uint8Array[0] = 255
uint8Array[1] = 257
uint8Array[2] = 'hoge'
console.log(uint8Array)
// Uint8Array(16) [255, 1, 0]
上記から、Uint8Arrayオブジェクト作成直後は、各要素は0で初期化されており、255までの整数を設定できるが、範囲外の値の場合は、下位ビットの値のみが採用されていることがわかる。また、文字列を設定しようとしてもエラーにはならないが無視されるみたい。
型付き配列は、通常配列とどう違うの?
- Array.isArrayメソッドでfalseとなる
- pushやpopなどの一部のメソッドが使えない
const normalArray = [0, 1];
const buffer = new ArrayBuffer(16);
const typedArray = new Int16Array(buffer);
console.log(Array.isArray(normalArray));
// true
console.log(Array.isArray(typedArray));
// false
normalArray.push(2);
console.log(normalArray);
// [0, 1, 2]
typedArray.push(2);
// TypeError: typedArray.push is not a function
型付き配列の方が通常配列より速いってほんとに~?
下記のようなスクリプトを用意する。
const TRIAL_COUNT = 3;
const ARRAY_LENGTH = 1024*1024*50;
const DIVISOR = 200; // 各要素に格納する値は0~200になるようにする
//Array:
for(let j = 0; j < TRIAL_COUNT; j++) {
const startTime = Date.now();
let arr = Array(ARRAY_LENGTH);
for(let i = 0; i < arr.length; i++) {
arr[i] = i % DIVISOR;
}
let sum = 0;
for(let i = 0; i < arr.length; i++) {
sum += arr[i];
}
console.log('Array:', Date.now() - startTime, '[ms] , length:', arr.length, ', Sum:', sum);
arr = undefined;
}
//Uint8Array
for(let j = 0; j < TRIAL_COUNT; j++) {
const startTime = Date.now();
let arr = new Uint8Array(ARRAY_LENGTH);
for(let i = 0; i < arr.length; i++) {
arr[i] = i % DIVISOR;
}
let sum = 0;
for(let i = 0; i < arr.length; i++) {
sum += arr[i];
}
console.log('Uint8Array:', Date.now() - startTime, '[ms] , length:', arr.length, ', Sum:', sum, ', byteLen:', arr.byteLength);
arr = undefined;
}
//Float64Array:
for(let j = 0; j < TRIAL_COUNT; j++) {
const startTime = Date.now();
let arr = new Float64Array(ARRAY_LENGTH);
for(let i = 0; i < arr.length; i++) {
arr[i] = i % DIVISOR;
}
let sum = 0;
for(let i = 0; i < arr.length; i++) {
sum += arr[i];
}
console.log('Float64Array:', Date.now() - startTime, '[ms] , length:', arr.length, ', Sum:', sum, ', byteLen:', arr.byteLength);
arr = undefined;
}
実行すると、以下のように結果が得られた。
Array: 4561 [ms] , length: 52428800 , Sum: 5216665600
Array: 3715 [ms] , length: 52428800 , Sum: 5216665600
Array: 3761 [ms] , length: 52428800 , Sum: 5216665600
Uint8Array: 160 [ms] , length: 52428800 , Sum: 5216665600 , byteLen: 52428800
Uint8Array: 175 [ms] , length: 52428800 , Sum: 5216665600 , byteLen: 52428800
Uint8Array: 151 [ms] , length: 52428800 , Sum: 5216665600 , byteLen: 52428800
Float64Array: 138 [ms] , length: 52428800 , Sum: 5216665600 , byteLen: 419430400
Float64Array: 154 [ms] , length: 52428800 , Sum: 5216665600 , byteLen: 419430400
Float64Array: 157 [ms] , length: 52428800 , Sum: 5216665600 , byteLen: 419430400
この結果から以下が確認できる。
- 型付き配列の方が、通常配列よりも速い
- Float64とUint8では、速度はほとんど変わらない
- byteLenを見ると、Uint8Arrayの方がFloat64Arrayより少ないメモリで済んでいる
TypedArrayオブジェクト結局どれを使えばいいん?
Uint8ArrayでOK!
理由:
- ほとんどのバイナリファイルデータが8ビットの符号なし整数(バイト)単位で構成されている
- 必要最低限のビット幅をもつTypedArrayを使う方がメモリ効率がよい
Blob ってなに?
Blob(Binary Large Object)は、不変の生データであるファイルのようなバイナリデータの大きな塊を効率的に扱うためのオブジェクトらしい。
Blobは以下のように作成できる。Blobコンストラクタには、配列を渡す。配列の中身としては、文字列、ArrayBuffer, TypedArray, Blob などを入れる。
const obj = { hello: "world" };
const blob = new Blob([JSON.stringify(obj, null, 2)], {
type: "application/json",
});
Blob と File
下記のソースコードは、ファイルをアップロードすると、そのファイルを参照できるURLを生成し、画面表示する。
See the Pen Untitled by Yucca1 (@Yucca1) on CodePen.
- 上記のJavaScriptソースコードにおける、
event.target.files[0]
で取得できるものが File オブジェクト。 - Fileオブジェクトは、特別な種類のBlobオブジェクト。Blobが利用できる場面ではどこでも利用できる。
- 生成されるURLは、バイナリデータを保持していて、URL.createObjectURL を用いてFileオブジェクトを変換して取得している。
Blob と ArrayBuffer の使い分け
バイナリデータを扱う方法として、BlobとArrayBufferがあることはわかったけど、使い分けはどう考えればいいのか。どうやら以下のように考えてよさそう
- Blob: ファイルをそのまま操作
- ArrayBuffer:ファイルの中身をみてみる
例えば、二つのファイルをアップロードし、それらを結合して、ダウンロードするソースを書いてみると以下の通り。ここでは、ファイルのアップロード、ダウンロードはBlobで行い、ファイルの結合はArrayBufferを用いて実施している。
See the Pen BlobSample02 by Yucca1 (@Yucca1) on CodePen.
Base64 とは?
- Base64は、バイナリデータをテキスト形式で表現するためのエンコーディング方法
- バイナリデータを64種類のASCII文字(A-Z、a-z、0-9、+、/)で表現するため、テキストとして扱える
Base64 によるデータ転送
HTTPリクエスト・レスポンスボディでバイナリデータを送受信する場合に、Base64エンコーディングを使う。ここでは、HTTPリクエストでファイルデータを送信する準備として、画面上でアップロードしたファイルデータをBase64エンコーディングするところまでやってみる。
See the Pen BlobSample03 by Yucca1 (@Yucca1) on CodePen.
上記コードでは、FileReader オブジェクトを作成し、readAsArrayBuffer
メソッドを用いることで、ファイルの内容をArrayBufferとしてresult属性にセットしている。
また、btoaメソッド(binary to ASCII)により、バイナリ文字列をBase64エンコーディングしている。
Base64 によるデータ埋め込み
画像をBase64エンコードしてHTMLのタグのsrc属性に直接埋め込むこともできる。
See the Pen BlobSample03 by Yucca1 (@Yucca1) on CodePen.
上記コードでは、FileReaderオブジェクトのreadAsDataURL
メソッドを用いることで、ファイルの内容をデータURLとしてresult属性にセットしている。
参考文献
おわりに
人類は思い出した...バイナリは面倒ってことを...
とはいえ、基本的なことから学び、いろんなソースコードを書いてみて、また少しバイナリの扱い方について理解を深められた気がする。