【JavaScript】 型の異なるプロパティをバイナリで入出力する方法
更新日:2021/07/08
JavaScriptのArrayBufferは確保したバッファー内でバイナリデータを取り扱うことができます。
しかし複数の型データを読み書きしたい場合、書き込み位置(オフセット)を的確に管理する必要があります。
そこで今回は、ArrayBuffer上で複数の型データを管理するオブジェクトを作成してみます。
概要
今回は次のような仕様で、コードを作成してみます。
- 任意のプロパティを設定可能
- プロパティはTypedArrayの型を持つ
- プロパティの設定は、コンストラクターのみでおこなう
- プロパティへの値は、内部のArrayBufferで保持する
- bufferプロパティでArrayBufferを返す
- bufferプロパティにArrayBufferを代入すると、内部のArrayBufferにコピーする
実用レベルのものを作成するとコードが長くなってしまうので、次の制限をいれます。
- プロパティの型は、32ビット以下の整数のみ
- 入力値などのチェックはおこなわない
- bufferプロパティ代入時、入力ArrayBufferの長さを考慮しない
外部ファイルへの保存や読み込みは、bufferプロパティに関連付けられたArrayBufferを次のページを参考にして処理してください。
完成コード
最初はかなり長いものになったけれど、必要最低限に減らしたコードがこちら。
const BinaryObject = ( ( unitDefine )=>{
const dataMap = new WeakMap();
/**
* コンストラクター
* @param defineArray [ [ propName,propType ],[ propName,propType ],...]
*/
const construct = function ( defineArray = null){
// 総バイト数を計算しDataViewを作成
const dataLength = defineArray.reduce( (a,b)=>a+unitDefine[b[1]].byte , 0 );
const dataView = new DataView( new ArrayBuffer(dataLength) );
const propData = {};
let count = 0;
// プロパティを作成
defineArray.forEach( e =>{
const [ propName,propType ]= e;
const {byte,setFunc,getFunc} = unitDefine[propType];
const getData = dataView[getFunc].bind(dataView,count);
const setData = dataView[setFunc].bind(dataView,count);
propData[propName] ={ getData:getData,setData:setData };
count += byte;
Object.defineProperty(this,propName,{
enumerable:true,
get() {return getData();},
set( value ){setData(value);}
});
});
// WeakMapでデータ保持
dataMap.set( this , Object.freeze({dataView:dataView,propData:propData} ) );
};
// bufferプロパティの設定
Object.defineProperty(construct.prototype,"buffer",{
get() { return dataMap.get(this).dataView.buffer; },
set( arrayBuffer ){
const u8 = new Uint8Array( arrayBuffer );
const dataView = dataMap.get(this).dataView;
const length = Math.min(dataView.byteLength,u8.byteLength);
for( let i = 0 ; i < length ; i ++)
dataView.setUint8( i , u8[i] );
}
});
// コンストラクターにタイプ定数を追加
Object.keys( unitDefine ).forEach( e=>construct[e]=e );
return Object.freeze( construct );
})({
Int8:{byte:Int8Array.BYTES_PER_ELEMENT, setFunc:"setInt8", getFunc:"getInt8"},
Uint8:{byte:Uint8Array.BYTES_PER_ELEMENT, setFunc:"setUint8", getFunc:"getUint8"},
Int16:{byte:Int16Array.BYTES_PER_ELEMENT, setFunc:"setInt16", getFunc:"getInt16"},
Uint16:{byte:Uint16Array.BYTES_PER_ELEMENT, setFunc:"setUint16", getFunc:"getUint16"},
Int32:{byte:Int32Array.BYTES_PER_ELEMENT, setFunc:"setInt32", getFunc:"getInt32"},
Uint32:{byte:Uint32Array.BYTES_PER_ELEMENT, setFunc:"setUint32", getFunc:"getUint32"},
});
オブジェクト名が思いつかなかったので、BinaryObjectという適当なものになっています。
このコードの使用例が、こちら。
使用例
// BinaryObjectを作成
const bf = new BinaryObject([
["item1",BinaryObject.Uint8],
["item2",BinaryObject.Uint16],
]);
// プロパティに値をセット
bf.item1 = 0x1234;
bf.item2 = 0x5678;
// ArrayBufferを取得し内容確認
console.log( bf.buffer ); // ArrayBuffer { [Uint8Contents]: <34 56 78>, byteLength: 3 }
// 外部バッファーを受け入れ
bf.buffer = Uint8Array.of( 0xaa,0xbb ).buffer;
// ArrayBufferを取得し内容確認
console.log( bf.buffer ); // ArrayBuffer { [Uint8Contents]: <aa bb 78>, byteLength: 3 }
BinaryObjectのコンストラクターは、配列[プロパティ名,プロパティタイプ]の配列を引数として受け取り、プロパティを作成します。
プロパティタイプには"Int8"などの文字列を指定します。
実際にはBinaryObject.Uint8のように、あらかじめプロパティとして用意されているので、こちらを使用します。
コンストラクターで作成したオブジェクトのプロパティには、自由に値を設定できます。
ただし指定した型に丸められます。
bufferプロパティへのセットは、単純なコピーです。
コピー元の長さが短い場合は、元のデータが残ります。
簡単な解説
構造
このコードは、次のような即時関数です。
const BinaryObject = ( ( unitDefine )=>{
// コード
return Object.freeze( construct );
})( { } );
即時関数内で様々な処理を行い、最後に変数constructを返し、その値がBinaryObjectにセットされます。
Object.freezeしているのは、プロパティを後から変更できないようにするためです。
即時関数についてはこちらをご覧ください。
■【JavaScript】 即時関数の挙動について調べてみた
Object.freezeについてはこちらをご覧ください。
■【JavaScript】 オブジェクトのプロパティ追加を禁止する方法
コンストラクター
変数constructは、オブジェクトインスタンスを初期化するためのコンストラクターがセットされています。
コンストラクターでは、受け取った引数を元に必要なバイト数を計算してDataViewを作成しています。
バイト数を計算
const dataLength = defineArray.reduce( (a,b)=>a+unitDefine[b[1]].byte , 0 );
reduceは、配列の要素を順番に処理して一つの結果を取得するメソッドです。
reduceについてはこちらをご覧ください。
■【JavaScript】 forEach/map/filter/reduceを根本的に理解する
constキーワードの後に[ ]や{ }が続くのは、分割代入というJavaScriptの構文です。
分割代入
const [ propName,propType ]= e;
const {byte,setFunc,getFunc} = unitDefine[propType];
分割代入についてはこちらをご覧ください。
■【JavaScript】 分割代入はどこが便利なのか
bindは、関数にthis値と引数を関連付けることができるメソッドです。
ここではDataViewに入出力するメソッドに、this(DataView自身)とオフセットを関連付けています。
bindでthis値とoffsetを固定
const getData = dataView[getFunc].bind(dataView,count);
const setData = dataView[setFunc].bind(dataView,count);
bindについてはこちらをご覧ください。
■【JavaScript】 今更だがbind()について理解してみる
Object.definePropertyは、オブジェクトにプロパティを設定するメソッドです。
ここではコンストラクターで生成するオブジェクト(this値)に、プロパティを設定しています。
インスタンスにプロパティを設定
Object.defineProperty(this,propName,{
enumerable:true,
get() {return getData();},
set( value ){setData(value);}
});
definePropertyについてはこちらをご覧ください。
■【JavaScript】 definePropertyメソッドとは?通常のプロパティ追加との違い
最後にWeakMapに、this値をキーとしてDataViewなどをセットしています。
これはプロトタイプチェーンで呼び出されるメソッドでも、DataViewを使用するための仕組みです。
今回はコンストラクターが呼び出されるたびに、専用のDataViewが作成されます。
このDataViewをプロトタイプメソッドで使用するための方法として、this値にプロパティとして追加することが考えられます。
this値にDataViewを追加
this.dataView = dataView;
しかしDataViewを外部に公開しているため、自由にデータを変更できてしまいます。
そのため、DataViewを外部から隠蔽する手法としてWeakMapを使用しています。
WeakMapについてはこちらをご覧ください。
■【JavaScript】 WeakMapでオブジェクトとデータの関連付けをおこなう
更新日:2021/07/08
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。