为什么 atob() 返回字节,而 btoa() 会拒绝中文

理解 JavaScript 字节字符串、字符超范围错误,以及 atob、btoa 前后的 UTF-8 转换。

atob 和 btoa 的名字容易让人以为它们直接处理普通文本。浏览器中的实际约定是:在 Base64 文本和每个字符代表一个字节的字符串之间转换。这个“字节字符串”不保证是可读 Unicode 文本。

两种含义不同的字符串

JavaScript 字符串采用 UTF-16 码元。字节字符串则要求每个字符值在 0–255 范围内。ASCII 字符符合要求,所以 btoa('Hello') 可以执行;中文超出一个字节,btoa('你好') 会报错。即使字符能放进一个字节,也不代表使用了 UTF-8。例如 é 对应单字节 E9,而 UTF-8 编码需要 C3 A9。

console.log(btoa('é')); // 6Q==,单字节 E9
console.log(btoa(String.fromCharCode(0xc3, 0xa9))); // w6k=,UTF-8

在字节转换前后处理 UTF-8

使用 TextEncoder 把文字变成 UTF-8 字节,解码后用带 fatal: true 的 TextDecoder 还原文本。这样遇到非法序列会明确失败,而不是插入替换字符。

const text = '中文😊';
const bytes = new TextEncoder().encode(text);
let binary = '';
for (const byte of bytes) binary += String.fromCharCode(byte);
const encoded = btoa(binary);
console.log(encoded); // 5Lit5paH8J+Yig==
const decodedBytes = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(decodedBytes);
console.log(decoded); // 中文😊

二进制不等于乱码

atob('/w==') 得到 FF 字节,它不是合法的单独 UTF-8 字符,但仍然是合法二进制。应通过 Hex 视图检查或按字节下载。将它再次交给 TextEncoder,会把 U+00FF 编成另一组字节,造成数据变化。

浏览器解码器也不等于完整的协议校验器。严格场景还需检查字母表、填充和未使用位。大文件不要使用 String.fromCharCode(...bytes) 一次展开所有字节,这可能触及函数参数数量限制。接口定义见 MDN: atob() 和 MDN: btoa()。