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()。