ArrayBuffer
とTypedArray
はバイナリデータを扱うためのオブジェクトで、ES2015で標準化された。
この記事では、これらのオブジェクトの概要について述べたあと、Node.js でバイナリファイルを読み書きする方法についても説明する。
動作確認は Node.js のv10.9.0
で行っている。
データとインターフェース
バイナリデータはArrayBuffer
オブジェクトで表現する。
new ArrayBuffer(バイト単位のサイズ)
でArrayBuffer
のインスタンスを作ることが出来る。
const buf = new ArrayBuffer(8); console.log(buf); // ArrayBuffer { byteLength: 8 }
しかし、ArrayBuffer
インスタンスの中身を見たり操作したりすることは出来ない。
インスタンス自身は何のプロパティも持たないし、プロトタイプであるArrayBuffer
のprototype
にも、中身を操作するプロパティはない。byteLength
で長さを取得することくらいしか出来ない。
値は全て0
なのだが、それを確認することも出来ない。
const buf = new ArrayBuffer(8); console.log(buf); // ArrayBuffer { byteLength: 8 } console.log(buf[0]); // undefined console.log(Object.getOwnPropertyNames(buf)); // [] console.log(Object.getPrototypeOf(buf)); // ArrayBuffer {} console.log(Object.getOwnPropertyNames(ArrayBuffer.prototype)); // [ 'constructor', 'byteLength', 'slice' ] console.log(Object.getPrototypeOf(ArrayBuffer.prototype)); // {} console.log(buf.byteLength); // 8
バイナリデータの中身を読み書きする際には、TypedArray
オブジェクトを使う。
例えば、TypedArray
を継承しているUint8Array
にArrayBuffer
インスタンスを渡してUint8Array
インスタンスを作成すると、バイナリデータの中身を操作できるようになる。
const buf = new ArrayBuffer(8); const ta = new Uint8Array(buf); console.log(ta); // Uint8Array [ 0, 0, 0, 0, 0, 0, 0, 0 ] console.log(ta[0]) // 0 ta[0] = 1; console.log(ta[0]); // 1 console.log(ta); // Uint8Array [ 1, 0, 0, 0, 0, 0, 0, 0 ]
TypedArray
を直接使うことは出来ず、TypedArray
を継承しているオブジェクトを使う。
TypedArray // ReferenceError: TypedArray is not defined
TypedArray
を継承しているオブジェクトはUint8Array
以外にも複数あり、バイナリデータを配列としてどのように表現するのかが異なる。
例えばUint8Array
は8ビット(1バイト)ごとに符号なし整数で表現し、Uint16Array
は16ビット(2バイト)ごとに符号なし整数で表現する。
const buf = new ArrayBuffer(8); const u8 = new Uint8Array(buf); const u16 = new Uint16Array(buf); console.log(u8); // Uint8Array [ 0, 0, 0, 0, 0, 0, 0, 0 ] console.log(u16); // Uint16Array [ 0, 0, 0, 0 ]
TypedArray
を継承しているオブジェクトの一覧はこのページで見ることが出来る。
TypedArray
のインスタンスはbuffer
プロパティを使うことができ、コンストラクタを初期化する際に渡したArrayBuffer
のインスタンスを参照している。
const typedArray = Object.getPrototypeOf(Uint8Array); console.log(typedArray); // [Function: TypedArray] console.log(typedArray.prototype.hasOwnProperty('buffer')); // true console.log('buffer' in Uint8Array.prototype); // true const buf = new ArrayBuffer(8); const ta = new Uint8Array(buf); console.log(ta.buffer === buf); // true
1つのArrayBuffer
に対して複数のTypedArray
を作ることが出来るが、どのbuffer
も同じものを参照している。
そして、いずれかのTypedArray
で行った操作の結果は、他のTypedArray
にも反映される。
const buf = new ArrayBuffer(8); const arr1 = new Uint8Array(buf); const arr2 = new Uint8Array(buf); const arr3 = new Uint16Array(buf); // それぞれ別の TypedArray インスタンスだが…… console.log(arr1 === arr2); // false console.log(arr1 === arr3); // false console.log(arr2 === arr3); // false // 参照している ArrayBuffer は同じ console.log(buf === arr1.buffer); // true console.log(arr1.buffer === arr2.buffer); // true console.log(arr1.buffer === arr3.buffer); // true console.log(arr2.buffer === arr3.buffer); // true console.log(arr1[0], arr2[0], arr3[0]); // 0 0 0 arr1[0] = 9; console.log(arr1[0], arr2[0], arr3[0]); // 9 9 9
つまり、TypedArray
はバイナリデータのインターフェースを作成するものであり、インターフェースが複数あったとしても操作するバイナリデータは同じであると言える。
TypedArray
のインスタンスを作る際に数値を渡すと、その数の要素を持ったインスタンスが作られる。
console.log(new Uint8Array(1)); // Uint8Array [ 0 ] console.log(new Uint8Array(2)); // Uint8Array [ 0, 0 ]
この書き方をするとArrayBuffer
のインスタンスの作成を暗黙的に行う。
つまり、new Uint8Array(1)
とnew Uint8Array(new ArrayBuffer(1))
は同じことを行っている。
const arr1 = new Uint8Array(1); console.log(arr1); // Uint8Array [ 0 ] const arr2 = new Uint8Array(arr1.buffer); console.log(arr2); // Uint8Array [ 0 ] console.log(arr1.buffer === arr2.buffer); // true
fs モジュールを使ってバイナリファイルを読み書きする
ここからは Node.js 固有の話。
以前、フロントエンドでのバイナリファイルの取り扱いについて書いたが、それの Node.js 版である。
バイナリファイルの出力は単にfs.writeFile
でTypedArray
を書き出せばいい。
const fs = require('fs'); const arr = new Uint8Array(4); arr[0] = 4; arr[1] = 3; arr[2] = 2; arr[3] = 1; fs.writeFile('./foo', arr, err => { if (err) throw err; console.log('done!'); });
hexdump
コマンドで確認すると、正しく出力されているのを確認できる。
$ hexdump foo 0000000 04 03 02 01 0000004
ファイルの読み込みに使うのはfs.readFile
。
テキストファイルを読み込むときはfs.readFile(ファイルパス, エンコーディング, コールバック関数)
だが、バイナリファイルのときはエンコーディングは指定せずfs.readFile(ファイルパス, コールバック関数)
とする。
const fs = require('fs'); fs.readFile('./foo', (err, result) => { if (err) throw err; console.log(result); // <Buffer 04 03 02 01> });
ファイルの内容をもとにBuffer
インスタンスが作られる。
Buffer
はArrayBuffer
とは別のもので、EcmaScript で定義されたものではない。そのためブラウザ環境には存在しない。
const fs = require('fs'); fs.readFile('./foo', (err, result) => { if (err) throw err; console.log(result instanceof Buffer); // true console.log(result instanceof ArrayBuffer); // false });
実はBuffer
はUint8Array
を継承しており、そのため、値を操作することが出来る。
const fs = require('fs'); fs.readFile('./foo', (err, result) => { if (err) throw err; console.log(result instanceof Uint8Array); // true console.log(result[0]); // 4 result[0] = 9; console.log(result); // <Buffer 09 03 02 01> });