进制转换的本质:为什么计算机用二进制而人类用十进制
什么是进制转换?
进制转换,说白了就是把一个数从一种计数规则翻译成另一种计数规则。十六进制的 FF 和十进制的 255 是同一个数,只是穿的衣服不一样。
但为什么会有这么多套”衣服”?根源在于人类和计算机的底层硬件对数字的感知方式完全不同。我们天生有十根手指,十进制就自然成了人类的母语进制。计算机呢?晶体管只有通和断两种状态,所以它只认二进制。
于是问题来了:程序员要在人类和计算机之间做翻译。直接读二进制——比如 11111111——太费眼了。所以就有了八进制和十六进制,它们是二进制的”缩写语言”。一个十六进制字符正好对应 4 个二进制位,两个十六进制字符正好一个字节,不多不少。这个完美映射,就是 Hex 在编程世界里无处不在的根本原因。
我第一次真正意识到进制不是教科书里的抽象概念,是在配一台 Linux 服务器的时候。执行 chmod 755 script.sh 之后脚本就能跑了,我当时只知道”755 代表权限”,完全没去想为什么偏偏是这三个数字。后来才知道,这是八进制——7=111(读+写+执行),5=101(读+执行),每个数字恰好 3 个 bit,和权限位的 rwx 完美对应。这个设计简直漂亮。
工作原理:几套进制为什么长那样
四种进制速览
| 进制 | 基数 | 数码 | 前缀 | 用途 |
|---|---|---|---|---|
| 二进制 | 2 | 0, 1 | 0b | 位运算、底层硬件 |
| 八进制 | 8 | 0-7 | 0o | Unix 文件权限 |
| 十进制 | 10 | 0-9 | 无 | 人类日常 |
| 十六进制 | 16 | 0-9, A-F | 0x | 内存地址、哈希、数据 dump |
0b、0o、0x 这三个前缀不是数据本身,而是给编译器和读代码的人一个信号——“后面的数字请按特定进制理解”。
0b1010 // 二进制 → 十进制 10
0o644 // 八进制 → 十进制 420
0xFF // 十六进制 → 十进制 255
进制转换的通用算法
不管什么进制,转换的逻辑都一样——把生活里的十进制想象成”中转站”:
任意进制转十进制:按权展开。每一位的值乘上基数的对应次幂,全加起来。
二进制 1101 = 1×2^3 + 1×2^2 + 0×2^1 + 1×2^0 = 13
十六进制 2A = 2×16^1 + 10×16^0 = 42
八进制 77 = 7×8^1 + 7×8^0 = 63
十进制转任意进制:不断除以目标基数,记余数,倒序读。
42 转十六进制:
42 ÷ 16 = 2 余 10 → A
2 ÷ 16 = 0 余 2 → 2
结果:2A
这个算法七八十行代码就能写出来,但你大概率不需要自己写。JavaScript 内置了两个专门干这个的方法。
JavaScript 的 parseInt 和 toString
// 任意进制 → 十进制
parseInt("FF", 16); // 255
parseInt("1010", 2); // 10
parseInt("77", 8); // 63
// 十进制 → 任意进制(radix 范围 2-36)
(255).toString(16); // "ff"
(255).toString(2); // "11111111"
(255).toString(8); // "377"
有一个我自己踩过的坑——parseInt 在不传第二个参数的时候会自作聪明。比如 parseInt("077") 在某些旧引擎里会被当作八进制解析,返回 63 而不是 77。所以 parseInt 的 radix 参数实际上不应该省略——永远写成 parseInt(str, 10) 或指定你要的进制,别让引擎替你猜。说实话,ES5 之后已经废除了默认八进制的行为,但这不代表你可以偷懒——代码的可读性本身就是一种安全。
为什么 Hex 是程序员最常用的进制?
想象你在调试一段内存数据,有 4 个字节:10100111 11111111 00000000 01111111。盯久了眼睛疼。
写成 Hex:A7 FF 00 7F。两个字符一个字节,排列整齐,一眼能看出边界。写成十进制的话——乱七八糟,完全看不出字节结构。所以 hex dump 工具几乎清一色用十六进制加 ASCII 对照栏的布局,三栏一目了然。
八进制其实也有它的妙处,只是场景窄。3 位一组的设计让它天生适合表示 3-bit 的权限位。chmod 755 的 7 是 rwx(读 4 + 写 2 + 执行 1),5 是 r-x。换成十六进制反而别扭——你不会想写 chmod 0x1ED。所以不是 Hex 在所有场景都最优,是看你的数据结构和进制宽度是否对齐。
核心特性
| 特性 | 说明 |
|---|---|
| 本质 | 同一数值的不同表示形式,不改变数值本身 |
| 二进制与 Hex 的映射 | 4 bit = 1 Hex 字符,完美对齐字节边界 |
| 八进制与权限的映射 | 3 bit = 1 个八进制数码,天然对应 rwx |
| JavaScript 支持 | parseInt(str, radix) 和 num.toString(radix),radix 范围 2-36 |
| 前缀约定 | 0b 二进制、0o 八进制、0x 十六进制(ES6 标准化) |
| 可逆性 | 进制转换完全可逆,无信息损失,和加密/哈希不同 |
实际应用场景
1. Unix 文件权限(八进制)
前面提到的 chmod 755 本质上是一次八进制到二进制的隐式转换。每个数字展开成 3 位二进制,分别控制读(r=4)、写(w=2)、执行(x=1):
7 = 111 → rwx(所有者有全部权限)
5 = 101 → r-x(用户组有读和执行)
5 = 101 → r-x(其他人有读和执行)
八进制在这里比十六进制自然,因为 3 位刚好是一个权限组。这不是巧合,是特意选的设计。
2. 颜色代码(十六进制)
#FF5733 这样的 CSS 颜色码,背后是三个十六进制字节——红通道 FF(255)、绿通道 57(87)、蓝通道 33(51)。每个通道 8 位,正好两个 Hex 字符,共 24 位表示 1600 多万种颜色。
3. 内存地址和调试(十六进制)
debug 时看到的地址清一色是 Hex:0x7ffc8b3a2100。这不是装酷——内存地址天然是 2 的幂的倍数,用十六进制一眼能看出边界和范围,用十进制则会丢失这种结构感。
4. 位掩码和标志位(二进制)
JavaScript 里位运算并不常见,但它确实是一个干净的解——用单个整数承载多个布尔标志:
const READ = 0b001; // 1
const WRITE = 0b010; // 2
const EXEC = 0b100; // 4
let perm = READ | WRITE; // 0b011 = 3
const canExec = (perm & EXEC) !== 0; // false
二进制字面量 0b 前缀在这里让标志位的定义一目了然,比写十进制 1, 2, 4 清楚得多。
常见误区
误区一:进制转换会改变数值
不会。0xFF、255、0b11111111 是同一个数。改变的只是书写形式。这跟汇率换算有点像——100 人民币和 14 美元,币种不同,价值不变。进制就是数字的”币种”。
误区二:parseInt 不传第二个参数没关系
前面说过,这个习惯非常不好。parseInt("08") 在老引擎里可能返回 0(当成无效八进制),ES5 后虽然修了,但 parseInt("0x10") 仍然会被当成十六进制返回 16。如果你的输入来自用户,永远写成 parseInt(input, 10)。
误区三:进制前缀在所有语言里都一样
0x 作为十六进制前缀几乎是通用的,但八进制各语言的写法差别很大。C 语言里 077 就是八进制(一个前导零),Go 和现代 JS 用 0o77,Python 3 也是 0o77。我写过一阵 Go 之后回头写 C,在数字前面加了个 0o,编译直接报错——语言迁移时最容易在细节上翻车。
进制之间的对比
| 二进制 | 八进制 | 十进制 | 十六进制 | |
|---|---|---|---|---|
| 基数 | 2 | 8 | 10 | 16 |
| 单字节表示长度 | 8 字符 | 3 字符(不对齐) | 1-3 字符 | 2 字符 |
| 与字节的对齐 | 直接 | 否(3 bit 组) | 否 | 完美(4 bit 组) |
| 可读性 | 差 | 一般 | 最好(人类) | 好(程序员) |
| 典型场景 | 位运算 | chmod 权限 | 日常计算 | 内存、哈希、颜色 |
| JS 前缀 | 0b | 0o | 无 | 0x |
常见问题
Q: 为什么 JavaScript 的 toString(radix) 上限是 36?
因为 0-9 十个数字加上 a-z 二十六个字母,一共正好 36 个可用符号。高于 36 的基数需要发明新符号,语言设计者觉得没必要。
Q: 0xFF 和 \xFF 是一回事吗?
不是。0xFF 是数值字面量,\xFF 是字符串里的十六进制转义序列,表示 ASCII 码为 255 的那个字符。场景不同,语法不同。
Q: 八进制在 2026 年还有必要学吗?
如果你做后端或者运维,绝对有。Linux 文件权限、umask、某些嵌入式系统的寄存器定义,全是八进制。不学的话每次都要掏出计算器现查,效率极低。
Q: 写代码时到底该用哪种进制?
看数据结构的自然宽度。字节级别用 Hex(两个字符一个字节),3-bit 权限组用八进制,多个布尔标志用二进制字面量。一种进制不统治所有场景——够用就行,别教条。
Q: 浮点数可以做进制转换吗?
可以,但比整数麻烦得多。小数部分的转换是乘目标基数取整,而不是除基取余。而且十进制的小数转二进制经常出现无限循环——0.1 在二进制下是 0.0001100110011...,这就是 0.1 + 0.2 !== 0.3 的根本原因。