Base64 是程序员最熟悉的编码之一,但很多人只停留在会用 btoa 和 atob 的层面。它的设计并不复杂,但细节里藏着不少坑。这篇文章从头拆一遍 Base64,把规则、填充、变体、浏览器实现和性能问题都说清楚。
为什么需要 Base64
计算机里所有数据本质上是二进制字节序列。但很多传输通道只支持文本字符——最典型的就是电子邮件。早期 SMTP 协议是 7 位 ASCII 通道,高位字节会被剥离。如果你直接发一张图片的原始字节,数据到一半就坏了。
Base64 解决的就是这个问题:把任意二进制数据编码成只包含 64 个可打印字符的文本串。这 64 个字符分别是 A-Z、a-z、0-9、+ 和 /,加上填充符 =,总共 65 个字符。选这些字符的原因是它们在绝大多数编码系统中都有相同表示,不会出现乱码问题。
编码规则:3 字节变 4 字符
Base64 的核心逻辑是把 3 个字节(24 bits)拆成 4 组,每组 6 bits,然后映射到上面的字母表。
3 字节 = 24 bits。24 ÷ 6 = 4 组。这就是 3→4 的来源。
看一个具体例子。假设要编码三个字节:Man 的 ASCII 值是 77、97、110,二进制是:
01001101 01100001 01101110
把这 24 bits 分成 4 组,每组 6 bits:
010011 010110 000101 101110
每组 6 bits 对应一个 0-63 的数字:
010011 = 19
010110 = 22
000101 = 5
101110 = 46
查 Base64 字母表(A=0, B=1, ... Z=25, a=26, ..., +=62, /=63):
19 → T
22 → W
5 → F
46 → u
所以 Man 编码后是 TWFu。
反过来解码你也应该能想到了:每个字符找对应数值(6 bits),拼回 24 bits,再拆成 3 个字节。如果编码结果某一段数据不对,检查一下位运算的边界,很多人翻车在这里。
填充机制:等号从哪来
上面那个例子正好 3 个字节,完美对齐。但大多数情况下数据长度不是 3 的倍数,这时候就需要填充。
剩余 1 字节的情况:
假设只编码一个字节 M(ASCII 77):
01001101
补 0 到满 2 组 6 bits(不足的用 0 填充):
010011 010000
查表:TQ。但编码规则要求 4 个字符一组,长度必须是 4 的倍数。所以补两个 = 表示这里有 2 个填充字节:
TQ==
剩余 2 字节的情况:
编码 Ma(77, 97):
01001101 01100001
补成 3 组 6 bits:
010011 010110 000100
查表:TWE。补一个 =:
TWE=
解码时遇到 = 就知道要忽略多少填充数据。严格来说,= 不是必须的——编码后的字符数已经能推出原始数据长度。但 Base64 规范要求加上它,因为有些场景需要拼接编码结果,固定长度的格式更友好。
一个常见的误解:看到 = 就认为 Base64 编码一定以 = 结尾。这不对。只有数据长度不是 3 的倍数时才需要填充,很多情况下编码结果末尾根本没有 =。
字母表变体
标准 Base64 用 + 和 /。但这两个字符在 URL 里有特殊含义——+ 被解释为空格,/ 是路径分隔符。直接把标准 Base64 结果塞进 URL 会导致数据错误。
解决办法是 URL-safe Base64:用 - 替换 +,用 _ 替换 /,去掉末尾的 =。这样编码后的字符串可以直接用在 URL 里而不需要额外转义。
JWT(JSON Web Token)用的就是 URL-safe Base64。如果你解码过 JWT 的 payload 部分,看到的乱码字符和 -、_ 就是它。
另外还有 MIME 变体。标准 Base64 每 76 个字符插一个换行,方便在邮件中传输。大部分现代实现不会自动换行,但如果你在处理历史邮件数据,可能会遇到换行问题。
浏览器中的实现:btoa 和 atob
浏览器提供了两个原生方法:
btoa():binary to ASCII,编码atob():ASCII to binary,解码
名字起得不好,容易误解。实际上 btoa 不是把二进制数据编码成 Base64,它只接受 Latin-1 范围内的字符串。换句话说,btoa 接收的是"每个字符占 1 字节"的字符串,而不是任意二进制数据。
这是它最大的限制。如果你直接对 Uint8Array 或任意 JavaScript 字符串(UTF-16)调用 btoa,会抛出 InvalidCharacterError。
要编码真正的二进制数据(例如 ArrayBuffer),需要两步:
function base64Encode(buffer) {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
这套操作的性能瓶颈在字符串拼接。数据量大时(几 MB 以上),推荐用 TextDecoder 配合批处理,或者用 Web Worker 异步处理避免阻塞主线程。
解码时同样要注意。atob 返回的是字符串,要转回 Uint8Array:
function base64Decode(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
性能考量
Base64 编码后的体积比原始二进制大 33%(4/3 的比例)。这是编码方案本身的数学决定的,没有优化空间。但实现层面的性能差距很大。
字符串拼接 vs 数组预分配:
低效的做法是用 += 拼字符串。JavaScript 字符串不可变,每次 += 都创建新字符串,大文件场景下内存分配极其昂贵。
高效的做法是用数组或 Uint8Array 预分配空间:
function base64EncodeOptimized(bytes) {
const base64chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const len = bytes.length;
const result = new Uint8Array(Math.ceil(len / 3) * 4);
let i = 0,
j = 0;
while (i < len) {
const b1 = bytes[i++],
b2 = bytes[i++] || 0,
b3 = bytes[i++] || 0;
const triple = (b1 << 16) | (b2 << 8) | b3;
result[j++] = base64chars.charCodeAt((triple >> 18) & 0x3f);
result[j++] = base64chars.charCodeAt((triple >> 12) & 0x3f);
result[j++] = base64chars.charCodeAt((triple >> 6) & 0x3f);
result[j++] = base64chars.charCodeAt(triple & 0x3f);
}
// 处理填充
const pad = len % 3;
if (pad) {
// 从末尾开始替换为 =
for (let k = 0; k < 3 - pad; k++) {
result[j - 1 - k] = 61; // '=' 的 ASCII 码
}
}
return new TextDecoder().decode(result);
}
这段代码预分配了精确大小的 Uint8Array,避免了增量字符串拼接的开销。对几百 KB 到几 MB 的数据有明显改善。
原生方法 vs JS 实现:
浏览器的 btoa 和 atob 是原生实现的(C++ 层面),速度远超任何纯 JS 实现。能用它们的时候尽量用。它们的局限只是不能直接处理 ArrayBuffer,但用上面包装一下就好了。
在 Node.js 端,Buffer.toString('base64') 和 Buffer.from(str, 'base64') 是最优选择。Node.js 的 Buffer 实现直接调了底层 OpenSSL,比任何第三方库都快。
实际应用场景
Base64 最常见的几个用途:
在 URL 中传二进制数据。 比如图片转 Base64 直接嵌入 HTML 的 <img src="data:image/png;base64,...">。这种做法能减少 HTTP 请求数,但代价是体积膨胀 33%,且不会被浏览器缓存。适合小图标和 logo,不适合大图。
JWT 的载荷部分。 JWT 的 header 和 payload 用 URL-safe Base64 编码。注意 JWT 不加密——Base64 只是编码,任何人都能解码读取内容。不要在 JWT 里放敏感数据。
邮件附件。 MIME 协议用 Base64 编码二进制附件,这是 Base64 最原始的使用场景。现代邮件客户端都支持,但编码后的邮件体积比原始附件大三分之一。
存储二进制到文本型数据库。 有些数据库或数据结构只支持文本字段,Base64 是通用的二进制序列化方案。
最后
Base64 的核心逻辑很简单:3 字节变 4 字符,查表映射,不足补等号。注意事项反而比原理多——URL-safe 变体、填充规则、浏览器的 Latin-1 限制、大数据的性能优化。理解这些细节后,在项目里用 Base64 就不会踩坑了,平时也可用一些Base64 编解码工具查看编码前原始内容,比自己写一段代码解码要方便很多。