Base64 的等号到底从哪来

2026-01-27编码转换

Base64 是程序员最熟悉的编码之一,但很多人只停留在会用 btoaatob 的层面。它的设计并不复杂,但细节里藏着不少坑。这篇文章从头拆一遍 Base64,把规则、填充、变体、浏览器实现和性能问题都说清楚。

为什么需要 Base64

计算机里所有数据本质上是二进制字节序列。但很多传输通道只支持文本字符——最典型的就是电子邮件。早期 SMTP 协议是 7 位 ASCII 通道,高位字节会被剥离。如果你直接发一张图片的原始字节,数据到一半就坏了。

Base64 解决的就是这个问题:把任意二进制数据编码成只包含 64 个可打印字符的文本串。这 64 个字符分别是 A-Za-z0-9+/,加上填充符 =,总共 65 个字符。选这些字符的原因是它们在绝大多数编码系统中都有相同表示,不会出现乱码问题。

编码规则:3 字节变 4 字符

Base64 的核心逻辑是把 3 个字节(24 bits)拆成 4 组,每组 6 bits,然后映射到上面的字母表。

3 字节 = 24 bits。24 ÷ 6 = 4 组。这就是 3→4 的来源。

看一个具体例子。假设要编码三个字节:Man 的 ASCII 值是 7797110,二进制是:

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 实现:

浏览器的 btoaatob 是原生实现的(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 编解码工具查看编码前原始内容,比自己写一段代码解码要方便很多。