JavaScriptでフラグをビットで管理する~複数の状態フラグを少ない容量で管理

状態を管理する、その状態に応じて条件分岐するやり方。

フラグをビットで管理するメリットとケース

特定の条件でのみリマーケティングタグを発火する、イベントをカウントするなど、何らかの処理をすることがある。

「3回目の訪問で、詳細ページを閲覧済みで、未購入のユーザに対して広告配信をしたい」

というケースなど、条件が複雑になると広告配信プラットフォームのタグと管理画面だけでは実現不可能で、JavaScriptとcookieを使う必要がある。

個別の条件を満たしたときにJavaScriptで条件判定をし、フラグを立てる。
その結果をcookieに保存する。
cookieに保存されたフラグの組み合わせを参照して、すべての条件を満たしていたら特定のタグを発火する。

ということで、状態のフラグ(3回目の訪問かどうか、詳細ページを見たことがあるかどうか、未購入かどうか)を管理することになる。
単純に1フラグを1cookie変数とするとcookieで管理する変数の数が多くなってしまう。
そこで1個の変数でこれらの状態をまとめて管理する方法がビットによる管理である。

なおこの考え方はJavaScriptに限らずあらゆるプログラム言語で実装が可能。
管理するフラグが多く、メモリ消費や発生するパケットを小さくする必要があるときに重要となる。

ビット

ビットは0または1の2値を表す情報の単位
それを8個つなげたもの、

11001101

は8個の0/1を表す情報を保持している。
11001101の各桁は0か1しかないので2進数として見ることができる。
これを10進数に置き換えると205ということになる。

8ビットの変数1個(0~255の整数)であれば8個のビット(0/1)が収まる。
つまり0~255の整数で8種類の0/1の状態を表せる。

この最小の情報の単位であるビットに対する処理がビット処理である。

フラグの定義

var STATE = {
    'DISABLED': 1<<0,
    'HOVER': 1<<1,
    'ACTIVE': 1<<2
};

1<<桁で、桁は0からスタート

フラグを設定(代入)する

まず状態を表す変数を宣言

let state

フラグDISABLEDを立てる

state |= STATE.DISABLED

フラグDISABLEDHOVERを同時に立てる(一度に代入する)

state |= STATE.DISABLED | STATE.HOVER

フラグのセットを作る

state = STATE.DISABLED | STATE.HOVER | STATE.ACTIVE

フラグDISABLEDを下げる

state &= ~STATE.DISABLED

フラグを下げる場合は「&」にするのと否定の「~」を付ける

フラグDISABLEDHOVERを同時に下げる(一度に代入する)

state &= ~(STATE.DISABLED | STATE.HOVER);

特定のフラグを反転させる

state ^= STATE.DISABLED;

特定のフラグを取り出す

stateの中でDISABLEDに対応するビットの値を抜き出す(これが0でなければDISABLEDのフラグが立っていることになる)

state & STATE.DISABLED

stateの中でDISABLEDHOVERに対応する部分を抜き出す

state & (STATE.DISABLED | STATE.HOVER)
  • これが0でなければDISABLEDまたはHOVERのフラグが立っていることになる
  • これが(STATE.DISABLED | STATE.HOVER)と等しい場合、DISABLEDHOVERの両フラグが立っていることになる

フラグの取り出しは条件判定で使うことになる。

逆引き

やりたいこと 演算方法 演算子 変更したいビット そのままにしたいビット
ビットをすべて反転させたい NOT ~ すべて変更される 指定不可
一部のビットを反転させたい XOR ^ 反転させたいビット: 1 0
一部のビットをONにしたい OR | ONにしたいビット: 1 0
一部のビットをOFFにしたい AND & OFFにしたいビット: 0 1

https://codeiq.jp/magazine/2014/03/6558/

条件判定

if(state & STATE.DISABLED) { 処理 }
if(state & (STATE.DISABLED | STATE.HOVER)) { 処理 }
!!(state & STATE.DISABLED) && 処理

ビットを使った条件

// cookie処理ライブラリ
var _cookie = {
    set: function(cname, cvalue, exdays, path) {
        var strCookie = cname + '=' + cvalue;
        if (typeof exdays != 'undefined') {
            var d = new Date();
            d.setTime(d.getTime() + (exdays*24*60*60*1000));
            strCookie += '; expires=' + d.toUTCString();
        }
        strCookie += typeof path != 'undefined' ? '; path=' + path : '; path=/'
        document.cookie = strCookie;
    },
    get: function(cname) {
        var name = cname + '=';
        var ca = document.cookie.split(';');
        for(var i = 0; i < ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) == ' ') {
                c = c.substring(1);
            }
            if (c.indexOf(name) == 0) {
                return c.substring(name.length, c.length);
            }
        }
        return '';
    }
};
// ここから処理
var p = location.pathname;
var FLAGS = {
    HOME: 1<<0,
    CAR_TOYOTA: 1<<1,
    CAR_NISSAN: 1<<2,
    TAG_FIRED: 1<<3
};
var flags = parseInt(_cookie.get('pageview_state') || 0);
/^\/(index\.html)?$/.test(p) && (flags |= FLAGS.HOME);
p.indexOf('/toyota/car/') == 0 && (flags |= FLAGS.LIST_TOYOTA);
p.indexOf('/nissan/car/') == 0 && (flags |= FLAGS.LIST_NISSAN);
// トップページを閲覧したことがなく、日産かトヨタの車を見たことがある、かつタグ発火したことがない
if (!(flags & FLAGS.HOME) && (!!(flags & FLAGS.TOYOTA) || !!(flags & FLAGS.NISSAN)) && !(flags & FLAGS.TAG_FIRED)) {
    console.log('Fire a tag!');
    flags |= FLAGS.TAG_FIRED;
}
_cookie.set('pageview_state', flags);

参考

計測実装 の記事一覧