進数変換とビット演算 - 「なぜ16進数なのか」を実務目線で理解する
執筆: ありがっさまりょうた
デジタル道具屋を個人で開発・運用しています。記事は実装時の検証結果と公式仕様を照合して執筆しています。
本記事の執筆方針と編集ポリシーについてはAbout ページをご覧ください。
導入:16進数を読む場面は意外に多い
デバッグ中に 0xDEADBEEF のような値を目にすることがあります。これは「DEAD BEEF」と読める語呂合わせの値で、 メモリの初期化パターンや未使用領域のマーカーとして古くから使われてきました。 16進数をそのまま読めると、こうした値の意味をすぐに判断でき、デバッグの効率が上がります。
Web 開発でも 16進数は身近です。CSS のカラーコード #FF0000 は 16進数ですし、 Unicode の文字コードも U+3042 のように 16進数で表記されます。 「読めると便利」というより、文字化けやカラー計算の調査では「読めないと原因にたどり着けない」場面が少なくありません。
1. なぜ「16進数」なのか
コンピュータは内部的に 2進数で動作しますが、人間が 2進数の並びを直接読むのは負担が大きいものです。 同じ値でも 11111111(2進数)より FF(16進数)の方が桁数が少なく、誤読も減ります。 16進数は 4ビットをちょうど 1桁で表せるため、2進数との対応が機械的に取れる点が最大の利点です。
- 4ビット = 1桁:1バイト(8ビット)が 2桁できれいに表現できる
- 視認性:32 ビット値が 8桁、64 ビット値が 16桁。一目でビット幅がわかる
- 歴史的経緯:CPU レジスタの幅、メモリアドレス、文字コード表など、低レイヤの世界では事実上の標準
JavaScript では Number.prototype.toString(radix) と parseInt(string, radix) で相互変換できます。 次の例は、10進数の 255 が 2進数では 8桁、16進数では 2桁になることを示しています。
(255).toString(2) // → "11111111"
(255).toString(16) // → "ff"
(255).toString(8) // → "377"
parseInt('11111111', 2) // → 255
parseInt('ff', 16) // → 255
parseInt('0xff', 16) // → 255("0x" 接頭辞は基数 16 のとき許容される)8進数(octal)は、Unix のファイルパーミッション 0755 や chmod 644 で今も現役です。 3ビット = 1桁の対応で、rwx の3ビットを表現するのに都合が良かったため残っています。
2. ビット演算で書ける「権限フラグ」
権限管理をデータベースで設計する際、can_read、can_write、can_delete のように 権限ごとに列を追加する方法があります。権限の種類が少ないうちはこれで十分ですが、 フラグが 10 個、20 個と増えていく場合は、整数 1 つの各ビットにフラグを割り当てる方法の方がスキーマも処理も単純になります。
const READ = 0b0001; // 1
const WRITE = 0b0010; // 2
const DELETE = 0b0100; // 4
const ADMIN = 0b1000; // 8
// 「読み書き可能、削除不可、管理者でない」
const perm = READ | WRITE; // → 3 (0b0011)
// 権限チェック
const canDelete = (perm & DELETE) !== 0; // → false
const canWrite = (perm & WRITE) !== 0; // → true
// 削除権限を付与
const newPerm = perm | DELETE; // → 7 (0b0111)
// 削除権限を剥奪
const restored = newPerm & ~DELETE; // → 3この方式の利点は次の 2 点です。
- 保存が単純:データベースには整数 1 列を持つだけで済み、フラグを追加してもスキーマ変更が不要
- 判定が高速:権限チェックは
AND演算 1 回で完了し、複数列を参照する必要がない
Linux のファイルパーミッション(rwx を 3 ビットで表現する)も、これと同じ仕組みで動いています。
注意:JavaScript のビット演算は 32bit まで
JavaScript のビット演算子は符号付き 32bit 整数に変換して動作します。 32 ビット目(最上位)に立ったビットは負数として扱われ、思わぬ結果になります。 例えば 1 << 31 の結果は 2147483648 ではなく -2147483648 です。 32 個を超えるフラグを扱う場合は BigInt を使うか、複数の整数に分割しましょう。
3. 文字コードを読み解く
絵文字や漢字が文字化けして「?」になったとき、原因究明には Unicode コードポイントを 16進数で読む力が要ります。
'あ'.codePointAt(0).toString(16)
// → "3042" (Unicode U+3042)
'😀'.codePointAt(0).toString(16)
// → "1f600" (Unicode U+1F600)
// UTF-8 では何バイト?
new TextEncoder().encode('あ').length // → 3
new TextEncoder().encode('😀').length // → 4「絵文字を文字数カウントしたら 2 になった」「DB の VARCHAR に絵文字が入らない」といった問題は、 サロゲートペアを 1 文字として扱えていないことが原因です。 JavaScript の文字列は UTF-16 で表現されるため、U+FFFF を超えるコードポイントは 上位サロゲート(0xD800〜0xDBFF)と下位サロゲート(0xDC00〜0xDFFF)の 2 つのコードユニットの組で格納されます。
'😀'.length // → 2(UTF-16 コードユニット数)
[...'😀'].length // → 1(コードポイント数)
'😀'.charCodeAt(0).toString(16) // → "d83d"(上位サロゲート)
'😀'.charCodeAt(1).toString(16) // → "de00"(下位サロゲート)文字数を数える際は length ではなく、スプレッド構文や Array.from でコードポイント単位に分割してから数えると、 絵文字を含む文字列でも期待どおりの結果になります。
4. 進数変換が日常で必要になるシーン
- カラーコード:HEX
#FF6347を RGB(255, 99, 71)に - 権限・ステータスフラグ:DB に保存された整数からビットを読み取る
- ネットワーク:IPv4 サブネットマスク、IPv6 アドレス
- セキュリティ:ハッシュ値、JWT のヘッダ・ペイロードを Base64URL でデコード
- 組込み・低レイヤ:レジスタ値の読解、メモリダンプ
5. 実際に変換してみる
即座に進数を変換するなら、進数変換ツールが便利です。 2進・10進・16進・8進の相互変換に加え、入力した数のビットパターンも視覚的に確認できます。
関連: カラーコード変換(HEX↔RGB)、ハッシュ生成(生成結果は通常 16進数文字列)。
よくある失敗パターン
JavaScript で parseInt('08') がブラウザによって 0 を返した時代がありました。 ES5 より前の仕様では、先頭が 0 の文字列を 8進数として解釈することが許容されていたためです。 第2引数(基数)を必ず明示する parseInt('08', 10) の習慣を付けると、 旧仕様への後方互換が原因のバグを未然に防げます。
参考にした一次情報
本記事の内容は、以下の公式仕様や一次情報を参照して執筆しています。
- ECMAScript Bitwise Operators
ECMA TC39
https://tc39.es/ecma262/#sec-bitwise-not-operator
- parseInt() - JavaScript | MDN
MDN Web Docs
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/parseInt
- The Unicode Standard, Code Charts
The Unicode Consortium
https://www.unicode.org/charts/