調べたのでメモ。
TypedArray
いわゆる型付き配列。
通常の配列と違って型が固定できる分、内部的に最適化がしやすいとか諸々で住み分けられてる。
そういう意味で一般的なフロントエンドのJavaScriptコードで出てくることはそうそうない。
たとえばWebAudioとか、CanvasとかWebGLとか、いわゆるバイナリに近い処理をブラウザでやる場合に必要になるAPIたち。
Node.jsだと、`Buffer`っていうそれ用のクラスがあったりする。
種類
- Int8Array
- Uint8Array
- Uint8ClampedArray
- Int16Array
- Uint16Array
- Int32Array
- Uint32Array
- Float32Array
- Float64Array
- BigInt64Array
- BigUint64Array
というように、並ぶアイテムの型によって名前が決まる。
Uint8の場合、符号なし8bit(1byte)なので、0から255までしか扱えない。
格納したいアイテムによって、使う種類を選ぶもの。
`Uint8ClampedArray`は特別なやつで、`255`より大きい値をいれると`255`に、`0`未満は`0`にしてくれるやつ。
ArrayBuffer
で、この`TypedArray`たちは、内部的なデータ構造としてそれぞれ`ArrayBuffer`を持つ。
というかデータの実態はこの`ArrayBuffer`であり、その見てくれ表現として各`TypedArray`がいる感じ。
const f32 = new Float32Array([0, 1, 2, 3, 4, 5, 6, 7]); f32.length; // 8 f32.buffer; // ArrayBuffer(32) f32.byteLength; // 32
で、共通した`ArrayBuffer`を持つということで、各`TypedArray`は実は相互変換ができる。
つまり`Uint8Array([0, 1])`と`new Uint16Array([256])`は同等の`ArrayBuffer`を持つということ。
const uint8 = new Uint8Array([0, 1]); const uint16 = new Uint16Array(uint8.buffer); new Uint8Array(uint16.buffer); // Uint8Array([0, 1]);
符号とかレンジとかはよしなになるっぽい。
// Uint8Array(7) [0, 6, 255, 0, 1, 255, 2] new Uint8Array( new Int8Array([-256, -250, -1, 0, 1, 255, 258]).buffer );
ただし要素数が足りない場合はエラーになる。
// Uncaught RangeError: byte length of Int16Array should be a multiple of 2 new Int16Array(new Int8Array([0, 1, 2]).buffer);
その裏側が知りたい
`Uint8Array([0, 1])`が`Int16Array([256])`と相互変換できるのは良いとして、この`256`ってどこから来た?って思ったので調べた。
- `256`を2進数に
- `(256).toString(2)`は、`100000000`
- `100000000`は、バイト列にすると`00000001|00000000`
- それが`00000000|00000001`となって(順序が逆になる)
- つまり`0`と`1`に
で、このバイト列がメモリ上で、CPU上で、どう扱われてるかにトリックがある。
この例でしれっと順序が逆になるって書いたのは、リトルエンディアン(LE)という方式を採用してバイトを並べるから。
いまこのコードを実行してるこのマシンが、そうなってるから。
Endianness - MDN Web Docs Glossary: Definitions of Web-related terms | MDN
ビッグエンディアン(BE)を採用してる環境だと、見た通りの順序で読み出される。
いや、そっちのほうがわかりやすいやん!って思ったけど、CPUの処理的な都合では、LEのほうに分があったりするらしい。
ただわかりやすいのも確かなので、プロトコルなどによっては絶対BEでやりとりすると決まってたりするらしい。
もう1つ例として、`Int32Array([50462976])`が`Int8Array`になる場合を見ておく。
// 10進数から2進数にしてみる Number(50462976).toString(2); // 11000000100000000100000000 // バイト区切り(0埋め) 00000011|00000010|00000001|00000000 // LEなので 00000000|00000001|00000010|00000011 // 10進数 0, 1, 2, 3
というわけで、`Int8Array([0, 1, 2, 3])`になる。
どっちのエンディアンを採用してるかはこういうコードでも調べられる。
const isBE = new Uint8Array(new Uint32Array([0x12345678]).buffer)[0] === 0x12; const isLE = new Uint8Array(new Uint32Array([0x12345678]).buffer)[0] === 0x78;
DataView
- 配列のインデックスで読み書きするの大変
- エンディアンを考慮しつつ読み書きするのも大変
・・ということで存在するクラス。
`getInt8()`とか`setFloat32()`とか任意の単位で`ArrayBuffer`を操作できるやつ。
デフォルトでBEを採用してて、各メソッドの引数でLEを指定できるようになってる。
const buffer = new ArrayBuffer(2); new DataView(buffer).setInt16(0, 256, true /* littleEndian */);
Node.jsでいうところの`Buffer`みたいな存在。
まぁNode.jsだと`Buffer`も`DataView`もどっちも生えてるけど・・。