ビット演算でフラグをまとめる
このレッスンで分かること
- 公開・注目・新着のような、はい/いいえの設定を 1 つの数値にまとめられます
|で立てる、& ~で降ろす、^で切り替える、&で調べる、の 4 つで足ります- 判定の結果は
trueではなく数値なので、=== trueと比べてはいけません
ビット演算でフラグをまとめる とは
作品カードの表示オプションのように、独立した「はい/いいえ」がいくつも並ぶ設定を 1 つの数値で持ち、演算子で立て降ろしする書きかたです。
boolean を 3 つ持つのをやめる
作品カードに「公開」「注目」「新着」の 3 つの表示オプションを付けたいとします。素直に書けばこうなります。
const work = {
title: "プロフィールサイト",
published: true,
featured: false,
isNew: true
};これで何も問題は起きません。実際、多くの場面ではこれが正解です。ただし、オプションが増えるほど関数の引数が長くなり、「今どの組み合わせなのか」を 1 つの値として渡したり保存したりできません。そこで、3 つのオプションに別々の数値を割り当てて、足し合わせた 1 つの数値で持つ方法があります。
const PUBLISHED = 1;
const FEATURED = 2;
const NEW = 4;1、2、4 と倍々にするのが決まりです。こうすると、どの組み合わせを足しても結果が重複しません。5 なら 1 と 4、つまり公開かつ新着だと一意に決まります。3 つ全部なら 7 です。この数値のことをフラグと呼びます。
| flags | 公開 | 注目 | 新着 |
|---|---|---|---|
0 | いいえ | いいえ | いいえ |
1 | はい | いいえ | いいえ |
3 | はい | はい | いいえ |
5 | はい | いいえ | はい |
7 | はい | はい | はい |
なぜ倍々なのかは、数値を 2 進で見ると位が 1 つずつずれるからです。ただし、ここで 2 進の変換を覚える必要はありません。「1 から始めて倍にしていく」「足しても混ざらない」の 2 つだけ持っていれば、この先のコードは全部読めます。
4 つの演算子で立て降ろしする
フラグを操作する演算子は 4 つで足ります。どれも数値を返します。
let flags = PUBLISHED; // 1
flags = flags | FEATURED; // 立てる 3
flags = flags & ~FEATURED; // 降ろす 1
flags = flags ^ NEW; // 切り替え 5
const isPublished = flags & PUBLISHED; // 調べる 1| は「どちらかで立っていれば立てる」なので、既に立っているところに重ねても何も起きません。何度呼んでも結果が同じというのは、実は扱いやすい性質です。
~ は全部の位をひっくり返す演算子で、~FEATURED は「注目以外」を表します。それと & を取ると、注目だけが落ちて他は残ります。降ろすときは & ~ の 2 文字で 1 組と覚えてください。
^ は「片方だけ立っているときに立てる」ので、同じフラグに 2 回使うと元に戻ります。押すたびに切り替わるボタンの実装にちょうど合います。
& で調べたときに返るのは true ではなく、立っていればそのフラグの値、立っていなければ 0 です。
console.log(5 & PUBLISHED); // 1
console.log(5 & FEATURED); // 0
console.log(5 & NEW); // 4| やりたいこと | 書きかた | 結果 |
|---|---|---|
| 立てる | flags | FEATURED | 既に立っていても変わらない |
| 降ろす | flags & ~FEATURED | 立っていなくても変わらない |
| 切り替える | flags ^ FEATURED | 呼ぶたびに反転する |
| 調べる | flags & FEATURED | 立っていればその値、なければ 0 |
まとめて立てる、全部降ろす
| でつないだ値もフラグとして扱えます。よく使う組み合わせに名前を付けておくと、呼び出し側が読みやすくなります。
const ALL = PUBLISHED | FEATURED | NEW; // 7
const PICKUP = PUBLISHED | FEATURED; // 3
let flags = 0;
flags = flags | PICKUP; // 公開と注目を一度に立てる 3
flags = flags & ~ALL; // 全部降ろす 0複数のフラグをまとめて調べたいときは、目的によって書きかたが変わります。
const hasAny = (flags & PICKUP) !== 0; // どちらかが立っている
const hasAll = (flags & PICKUP) === PICKUP; // 両方立っている「どちらか」は 0 でないこと、「両方」は取り出した結果が元の組み合わせと一致すること、で判定します。この 2 つを取り違えると、条件がゆるすぎたり厳しすぎたりする不具合になります。
判定の結果を true と比べない
ここがいちばん事故の多いところです。& の結果は数値なので、=== true は絶対に成り立ちません。
if ((flags & FEATURED) === true) { // 決して通らない
}
if (flags & FEATURED) { // 正しい。0 は falsy、それ以外は truthy
}
const featured = (flags & FEATURED) !== 0; // boolean が欲しいならこう書くif の条件に直接置くぶんには、0 が falsy であることに任せて問題ありません。関数の戻り値やオブジェクトのプロパティに入れるときは、!== 0 を付けて true か false に変換してから渡します。
もう 1 つ、カッコを忘れると意図が変わります。& は === より優先順位が低いので、flags & FEATURED === 2 は flags & (FEATURED === 2) と解釈されます。比較と混ぜるときは必ずカッコで囲んでください。
フラグの立て降ろしを 1 か所の関数に集めるのが定石です。
|や& ~が画面のあちこちに散らばると、どこで何が変わったのか追えなくなります。「flags を受け取って新しい flags を返す関数」を 1 本用意し、外からはその関数だけを呼ぶ形にします。第3章の純粋関数の考えかたがそのまま当てはまります。
いつ使い、いつ使わないか
フラグは万能ではありません。使いどころを外すと、読みにくいだけのコードになります。
// 向いている
// 独立したはい/いいえが 3 つ以上あり、組み合わせを 1 つの値として渡したい
// 向いていない
// 選択肢が「下書き」「公開」「非公開」のように排他なとき。文字列 1 つで足りる排他な状態は status という文字列 1 つで表すほうが、後から状態を足すときも読むときも素直です。フラグが効くのは、それぞれが独立していて同時に成り立つ場合だけです。
なお、JavaScript のビット演算は 32 ビットの整数として扱われます。フラグを 31 個より多く並べる設計は避けてください。実務でそこまで増えることは、まずありません。
よくある間違い
+で立てる
flags = flags + FEATURED; // 既に立っていると 2 重に足されて壊れる
flags = flags | FEATURED; // 正しい3 + 2 は 5 になり、注目が降りて新着が立ったことになります。3 | 2 なら 3 のままです。
- 降ろすつもりで
^を使う
flags = flags ^ FEATURED; // 立っていなければ、逆に立ってしまう
flags = flags & ~FEATURED; // 確実に降ろす^ は切り替えです。今の状態に関係なく降ろしたいなら & ~ を使います。
&&と&を書き間違える
if (flags && FEATURED) { // FEATURED は常に 2 なので、flags が 0 以外なら必ず通る
}
if (flags & FEATURED) { // 正しい
}1 文字違いで、常に真になる条件ができあがります。フラグを扱っているファイルでは特に見落としやすいので、書いたあとに読み返してください。
要件
- 関数名は applyBadge。引数は flags (現在の数値)、action (操作の種類)、badge (対象のフラグ) の 3 つ
- action が on なら立てる、off なら降ろす、toggle なら切り替える。それ以外の action では flags を変えない
- 戻り値は { flags, labels }。flags は適用後の数値
- labels は立っているオプション名を published, featured, new の順に並べた配列。1 つも立っていなければ空配列
入出力例
applyBadge(1, "on", 2) → {"flags":3,"labels":["published","featured"]}
applyBadge(7, "off", 4) → {"flags":3,"labels":["published","featured"]}
applyBadge(3, "toggle", 2) → {"flags":1,"labels":["published"]}
applyBadge(5, "resize", 2) → {"flags":5,"labels":["published","new"]}
applyBadge(0, "off", 1) → {"flags":0,"labels":[]}