【JavaScript】 ビット単位でデータ管理をする方法
更新日:2020/08/06
昔はフラグなどの0か1で済んでしまうデータはビットで管理していました。
メモリの価格が安くなりコンピューターに大容量で搭載するようになった現在は、ほぼ忘れられたテクニックです。
しかし最近になって、JavaScriptでビットを取り使う機会があったので、備忘録的にまとめておきます。
JavaScriptの2進数やビットに関する情報
まずはJavaScriptにおけるビットに関する演算について記載しておきます。
2進数リテラル
2進数を変数に直接代入するときは、0bを先頭に付加します。
下の例は、全て10進数で100を代入しています。
数値リテラル
const bin = 0b1100100; // 2進数
const dec = 100; // 10進数
const hex = 0x64; // 16進数
const oct = 0o144; // 8進数
console.log( bin , dec , hex , oct); // 100 , 100 , 100 , 100
2進数リテラルに、0または1以外を使用するとエラーになります。
const bin = 0b1100103; // Uncaught SyntaxError: Invalid or unexpected token
2進数文字列化
数値に対してtoString()メソッドを使用すると、文字列に変換してくれます。
その際引数に2を指定することで、2進数文字列に変換することができます。
基数を指定して文字列化
console.log( bin.toString(2) , dec.toString(10) , hex.toString(16) , oct.toString(8));
// 1100100 100 64 144
桁を揃えたいときは、ゼロサプレスしてください。
桁ぞろえ(ゼロサプレス)
console.log( ("00000000" + bin.toString(2)).slice( -8 ) );
// 01100100
なお基数を指定しても、マイナス値はマイナスで表示されます。
マイナス値の文字列化
console.log( ( -10 ).toString(2) ); // -1010
console.log( ( -10 ).toString(16) ); // -a
当たり前のように感じますが、ビット演算の結果を表示しようとすると少し混乱してきます。
-10は、メモリ上では16進数でfffffff6です。
この値を表示してほしいケースがあります。
その場合は、後述のビット演算の結果をunsignedに変換を見てください。
ビット演算の前に32ビット整数に変換される
ビット演算は、対象となる値を32ビットの整数に変換してから、計算が行われます。
整数変換の際、33ビット以上は切り捨てられます。
また32ビット目が1の場合、マイナス値として扱われます。
ビット演算子による32ビット整数化
const a = 0xF80000000; // 10進数:66571993088
console.log( a | 0 ); // -2147483648
ビット演算( | )の前に、16進数F80000000は、32ビットに変換されます。
変換後の値は、80000000です。
ビット演算( | )はビット論理和で、0を指定すると元の値がそのまま保たれます。
よって結果は、80000000のまま変わりません。
80000000は32ビット目が1なので負数です。
10進数なら-2147483648です。
ビット演算後の比較に注意
ビット演算後の値を比較するとき、注意が必要です。
ビット演算結果の比較
const a = 0x80000000; // 10進数で2147483648
console.log( (a | 0) === 0x80000000 ); // false
console.log( (a | 0) ===2147483648 ); // false
console.log( (a | 0) === a ); // false
console.log( (a | 0) === -2147483648); // true
a | 0 の演算結果は、0x80000000です。
しかし、 (a | 0) === 0x80000000 は偽値となってしまいます。
一つ前の項目ビット演算の前に32ビット整数に変換されるを読んでいないと、とても不思議に感じるはずです。
偽値となる理由は、a | 0 の演算結果は32ビットなので、値が-2147483648です。
一方、リテラルの0x80000000は64ビットで評価されます。値は2147483648です。
イコールとならないため、偽値となったのです。
ビット演算の結果をunsignedに変換
符号なし右シフト演算子(>>>)を使用すると、32ビット値を符号なしとして扱うことができます。
一つ前の項目ビット演算後の比較に注意の例に対して適用すると、次のような結果となります。
ビット演算結果をunsignedに変換
const a = 0x80000000;
console.log( (a | 0) >>> 0 === 0x80000000 ); // true
console.log( (a | 0) >>> 0 === 2147483648 ); // true
console.log( (a | 0) >>> 0 === a); // true
console.log( (a | 0) >>> 0 === -2147483648); // false
| よりも >>> の方が優先順位が高いので、a | 0 に ( ) が必要です。
なおマイナス値をtoString()で出力すると、基数に関係なくマイナスで表示されます。
メモリ上のイメージで出力したい場合は、符号なし右シフト演算子を使用します。
メモリ上のイメージで値を出力
const a = -10;
console.log( ( a | 0 ).toString(2) ); // -1010
console.log( ( a | 0 ).toString(16) ); // -a
console.log( ( (a | 0)>>>0 ).toString(2) ); // 11111111111111111111111111110110
console.log( ( (a | 0)>>>0 ).toString(16) ); // fffffff6
ビット演算子
ビット演算子について解説します。
【&】ビット論理積 (AND)
&演算子は、対応するビットが両方1のとき、1になります。
両方0のとき、0。
異なるときは0です。
ビット論理積の例
console.log( ("00" + ( 0b010 & 0b010).toString(2)).slice(-3) ); // 010
console.log( ("00" + ( 0b010 & 0b000).toString(2)).slice(-3) ); // 000
console.log( ("00" + ( 0b000 & 0b010).toString(2)).slice(-3) ); // 000
console.log( ("00" + ( 0b000 & 0b000).toString(2)).slice(-3) ); // 000
【|】ビット論理和 (OR)
| 演算子は、対応するビットが両方0のとき、0になります。
両方1のとき、1。
異なるときは1です。
ビット論理和の例
console.log( ("00" + ( 0b010 | 0b010).toString(2)).slice(-3) ); // 010
console.log( ("00" + ( 0b010 | 0b000).toString(2)).slice(-3) ); // 010
console.log( ("00" + ( 0b000 | 0b010).toString(2)).slice(-3) ); // 010
console.log( ("00" + ( 0b000 | 0b000).toString(2)).slice(-3) ); // 000
【^】ビット排他的論理和 (XOR)
^演算子は、対応するビットが同じ値のとき、0になります。
異なるなら1になります。
ビット排他的論理和の例
console.log( ("00" + ( 0b010 | 0b010).toString(2)).slice(-3) ); // 000
console.log( ("00" + ( 0b010 | 0b000).toString(2)).slice(-3) ); // 010
console.log( ("00" + ( 0b000 | 0b010).toString(2)).slice(-3) ); // 010
console.log( ("00" + ( 0b000 | 0b000).toString(2)).slice(-3) ); // 000
【~】ビット否定演算子
~演算子は、右辺(右側)の値をビット毎に反転します。
32ビット整数で評価されることに注意してください。
ビット排他的論理和の例
console.log( ("0".repeat(32) + ( ~ 0b010 >>> 0).toString(2)).slice(-32) );
// 11111111111111111111111111111101
console.log( ("0".repeat(32) + ( ~ 2 >>> 0 ).toString(2)).slice(-32) );
// 11111111111111111111111111111101
ビット演算は32ビットで行われるため、ここでは32桁表示しています。
【<<】ビット左シフト演算子
ビット左シフト演算子は、数値を32ビット整数に変換後ビットを左向きにシフトします。
空いたビットには0がセットされます。
ビット左シフト演算子の例
const a = 0xf;
console.log( ( "0".repeat(32) +( a >>>0 ).toString(2)).slice(-32) );
// 00000000000000000000000000001111
console.log( ( "0".repeat(32) + ( (a<<1) >>>0 ).toString(2)).slice(-32) );
// 00000000000000000000000000011110
console.log( ( "0".repeat(32) + ( (a<<2) >>>0 ).toString(2)).slice(-32) );
// 00000000000000000000000000111100
const b = 0x8000000f;
console.log( ( "0".repeat(32) +( b >>>0 ).toString(2)).slice(-32) );
// 10000000000000000000000000001111
console.log( ( "0".repeat(32) +( (b<<1) >>>0 ).toString(2)).slice(-32) );
// 00000000000000000000000000011110
console.log( ( "0".repeat(32) +( (b<<2) >>>0 ).toString(2)).slice(-32) );
// 00000000000000000000000000111100
【>>】ビット右シフト演算子
ビット右シフト演算子は、数値を32ビット整数に変換後ビットを右向きにシフトします。
空いたビットには、シフト前の最上位ビットと同じ値がセットされます。
ビット右シフト演算子の例
const a = 0xf;
console.log( ( "0".repeat(32) +( a >>>0 ).toString(2)).slice(-32) );
// 00000000000000000000000000001111
console.log( ( "0".repeat(32) + ( (a>>1) >>>0 ).toString(2)).slice(-32) );
// 00000000000000000000000000000111
console.log( ( "0".repeat(32) + ( (a>>2) >>>0 ).toString(2)).slice(-32) );
// 00000000000000000000000000000011
const b = 0x8000000f;
console.log( ( "0".repeat(32) +( b >>>0 ).toString(2)).slice(-32) );
// 10000000000000000000000000001111
console.log( ( "0".repeat(32) +( (b>>1) >>>0 ).toString(2)).slice(-32) );
// 11000000000000000000000000000111
console.log( ( "0".repeat(32) +( (b>>2) >>>0 ).toString(2)).slice(-32) );
// 11100000000000000000000000000011
【>>>】符号なしビット右シフト演算子
符号なしビット右シフト演算子は、数値を32ビット整数に変換後ビットを右向きにシフトします。
空いたビットには、0がセットされます。
変換後は、符号なし32ビット整数として扱われます。
符号なしビット右シフト演算子の例
const a = 0xf;
console.log( ( "0".repeat(32) + ( a >>> 0 ).toString(2)).slice(-32) );
// 00000000000000000000000000001111
console.log( ( "0".repeat(32) + ( a >>> 1 ).toString(2)).slice(-32) );
// 00000000000000000000000000000111
console.log( ( "0".repeat(32) + ( a >>> 2 ).toString(2)).slice(-32) );
// 00000000000000000000000000000011
const b = 0x8000000f;
console.log( ( "0".repeat(32) + ( b >>> 0 ).toString(2)).slice(-32) );
// 10000000000000000000000000001111
console.log( ( "0".repeat(32) + ( b >>> 1 ).toString(2)).slice(-32) );
// 01000000000000000000000000000111
console.log( ( "0".repeat(32) + ( b >>> 2 ).toString(2)).slice(-32) );
// 00100000000000000000000000000011
符号なし32ビット整数に対して、ビット演算をおこなうと符号ありになります。
const b = 0x8000000f;
console.log( b >>> 0 ); // 2147483663
console.log( b >>> 0 | 0); // -2147483633
ビットデータを取得・セットする方法
実際にビットデータを取得やセットをする方法をお伝えします。
ビットデータを取得
確認したいビットのみに1をセットした値を、用意します。
その値と、確認したいデータとのビット論理積を計算します。
結果が0なら、ビットは0です。
0以外なら、ビットは1です。
指定位置のビットを確認
const a = 0b0101;
// 右から3ビット目を確認
console.log( (a & 0b0100) ? 1 : 0 ); // 1
// 右から2ビット目を確認
console.log( (a & 0b0010) ? 1 : 0 ); // 0
複数のビットを一つのデータとして扱う場合、対応するビットの論理積を求めた後、符号なしビット右シフト演算子でシフトします。
複数のビットから値を取得
const a = 0b0101;
// 右から2,3ビットのデータを取得
console.log( (a & 0b0110) >>> 1 );
ビットデータをセット
ビットに1をセットする場合は、セットしたいビット位置のみ1をセットした値を用意します。
その値と対象データをビット論理和で演算します。
0をセットする場合は、セットしたいビット位置に0、その他に1をセットした値をを用意します。
その値と対象データをビット論理積で演算します。
指定位置のビットをセット
const a = 0b0101;
// 右から2ビット目に1をセット
console.log( ("0000" + ( a | 0b0010 ).toString(2)).slice(-4) ); // 0111
// 右から3ビット目に0をセット
console.log( ("0000" + ( a & 0b1011).toString(2)).slice(-4) ); // 0001
複数のビットを一つのデータとして扱う場合、対応するビットのみ0をセットしたデータとの論理積を求めます。
この結果、目的のビットがゼロクリアされます。
次に、論理和で値をセットします。
複数のビットに値をセット
const a = 0b0101;
// 右から2,3ビットに01をセット
console.log( ("0000" + ( a & 0b1001 | 0b0010 ).toString(2)).slice(-4) ); // 0011
ビットデータ管理サンプル
ビットデータ管理のサンプルプログラムです。
コード
ビットデータ管理ライブラリ
const BitManagement = function () {
if( BitManagement.bitDatas === undefined ){
Object.defineProperty(BitManagement,"bitDatas",{
value:(()=>{
const result=[],result2=[];
for( let i = 0 ; i < 32 ; i ++) {
const shift = 0x80000000 >>> i;
result.push( shift );
result2.push( ~shift >>> 0 );
}
return {
bits:result,
notBits:result2,
};
})()
});
Object.freeze(BitManagement.bitDatas.bits);
Object.freeze(BitManagement.bitDatas.notBits);
}
const bits = BitManagement.bitDatas.bits;
const notBits = BitManagement.bitDatas.notBits;
const data = new Uint32Array(1);
/**
* ビットデータを取得する
* @param bitPos ビット位置 上位0~31
* @param bitLength ビット長
* @returns {number}
*/
this.getData = (bitPos,bitLength=1) => {
if( bitPos < 0 || bitPos+bitLength > 31 ) throw new Error("BitManagement:Position out of range");
if( bitLength <= 1 ) return ( (data[0] & bits[bitPos])===0 ) ? 0 : 1;
let b = 0 | 0;
for( let i = 0 ; i < bitLength ; i ++ ) b |= bits[bitPos+i];
return ( (data[0] & b) >>> (31-(bitPos+bitLength-1)));
};
/**
* ビットデータをセットする
* @param bitPos ビット位置 上位0~31
* @param val セットする値
* @param bitLength ビット長
*/
this.setData = (bitPos,val,bitLength=1) => {
if( bitPos < 0 || bitPos > 31 ) throw new Error("BitManagement:Position out of range");
if( bitLength <= 1 )
data[0] = val!==0 ? data[0]|bits[bitPos] : data[0]¬Bits[bitPos];
else{
let b = 1 | 0;
for( let i = 0 ; i < bitLength ; i ++ ) b = (b << 1) | 1;
const v = val & b;
const shift = 31-(bitPos+bitLength-1);
data[0] = ( data[0] & ( ~(b << shift) ) | (v << shift ));
}
};
/**
* デバッグ用メソッド
*/
this.debug =()=>{
console.log( ("0".repeat(32) + data[0].toString(2)).slice(-32) );
}
};
初回実行時に、次のコードでビット演算に使用する固定データを作成しています。
固定データの作成
if( BitManagement.bitDatas === undefined ){
// コード省略
}
この処理で作成されるBitManagement.bitDatas.bitsは、次のようなデータが格納されます。
[0]:0b10000000000000000000000000000000
[1]:0b01000000000000000000000000000000
・・・
[31]:0b00000000000000000000000000000001
BitManagement.bitDatas.notBitsは、上の値をビット否定演算したものが格納されます。
[0]:0b01111111111111111111111111111111
[1]:0b10111111111111111111111111111111
・・・
[31]:0b11111111111111111111111111111110
次のコードは、書き換え可能なデータ領域を作成しています。
データ領域を作成
const data = new Uint32Array(1);
JavaScriptの変数は代入をおこなうと、実データを上書きせずに、新しく実データを作成します。
ビット演算をおこない、結果を変数にセットするときも同様です。
この特徴から、わざわざビットで管理しなくても、目的ごとに変数を用意した方が効率がいいです。
Uint32Arrayは、ArrayBufferという書き換え可能な領域を確保して、32ビット符号なし整数値への配列としてアクセスを可能とします。
使用法
コンストラクタを使用することで、32ビット分のデータ領域を確保します。
const bitData = new BitManagement();
.setData( bitPos , val , bitLength )メソッドは、ビットデータをセットします。
bitPos : ビットの位置。 上位ビットが0
val : セットする値。 ビット長より長いビットは切り詰められます
bitLength: 省略可能。セットするビットの長さです。
bitData.setData( 5 , 1 );
bitData.debug(); // 00000100000000000000000000000000
bitData.setData( 8 , 13 , 4 );
bitData.debug(); // 00000100110100000000000000000000
.getData( bitPos , bitLength )メソッドは、ビットデータを取得します。
bitPos : ビットの位置。 上位ビットが0
bitLength: 省略可能。取得するビットの長さです。
console.log( test.getData(5) ); // 1
console.log( test.getData(8,4) ); // 13
更新日:2020/08/06
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。