MENU

JavaScript配列・連想配列ArrayBuffer

【JavaScript】 型の異なるプロパティをバイナリで入出力する方法

更新日:2021/07/08

 

JavaScriptのArrayBufferは確保したバッファー内でバイナリデータを取り扱うことができます。
しかし複数の型データを読み書きしたい場合、書き込み位置(オフセット)を的確に管理する必要があります。

 

そこで今回は、ArrayBuffer上で複数の型データを管理するオブジェクトを作成してみます。

 

概要

 

今回は次のような仕様で、コードを作成してみます。

 

  1. 任意のプロパティを設定可能
  2. プロパティはTypedArrayの型を持つ
  3. プロパティの設定は、コンストラクターのみでおこなう
  4. プロパティへの値は、内部のArrayBufferで保持する
  5. bufferプロパティでArrayBufferを返す
  6. bufferプロパティにArrayBufferを代入すると、内部のArrayBufferにコピーする

 

実用レベルのものを作成するとコードが長くなってしまうので、次の制限をいれます。

 

  1. プロパティの型は、32ビット以下の整数のみ
  2. 入力値などのチェックはおこなわない
  3. 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でオブジェクトとデータの関連付けをおこなう

けーちゃんおススメJavaScript入門書

  • スラスラ読める JavaScript ふりがなプログラミング
  • プログラム未経験者がJavaScript始めるならコレ!
    コードを掲載して自分で理解しろという投げっぱなしな入門書とは異なり、コードに一つ一つどんなことをやっているかをふりがなという形式で解説しています。
    それでいてJavaScriptの基礎と応用を学べる良書です。
  • これからWebをはじめる人のHTML&CSS、JavaScriptのきほんのきほん
  • JavaScriptの機能を実践で活かすにはHTMLやCSSの知識が不可欠です。
    しかしそれらの知識があることが前提として書かれている書籍が多い中、この本は総合的な知識を身に着けることができます。
    HTMLやCSSの知識も不安な方には、ぴったりの一冊です
  •  

    入門書の役割は、自分のやりたいことをネットで調べることができるようになるための、基礎的な知識の獲得です。
    まずはこれらの本でしっかりと基礎知識を身につけましょう。
    そしてもっと高度なことや専門的なことはネットで調べ、情報が足りないと感じたら書籍を購入してください。


    期間限定情報:
    7/16から7/18は63時間のビッグセール!
    欲しかったアレが安く手に入るチャンスです
    忘れずにチェックしてください!
    僕は以前のタイムセール祭りで4Kモニタが買ったけど、それより安かったらどうしよう・・・

    さらにお得なポイントアップキャンペーンも同時開催!

    記事の内容について

     

    こんにちはけーちゃんです。
    説明するのって難しいですね。


    「なんか言ってることおかしくない?」
    たぶん、こんなご意見あると思います。

    裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。

    そんなときは、ご意見もらえたら嬉しいです。

    ご意見はこちら。
    https://note.affi-sapo-sv.com/info.php

    【お願い】

    お願い

    ■このページのURL


    ■このページのタイトル


    ■リンクタグ


    ※リンクして頂いた方でご希望者には貴サイトの紹介記事を作成してリンクを設置します。
    サイト上部の問い合わせよりご連絡ください。
    ただしサイトのジャンルによっては、お断りさせていただくことがあります。