btoa 和 atob:只支持 Latin-1
btoa(意为“binary to ASCII”)在编码字符串时,会把每个字符当作一个字节。atob 则反过来。对于 ASCII,这没有问题:
btoa("hello"); // "aGVsbG8="
atob("aGVsbG8="); // "hello"
但只要遇到码位 0–255 之外的字符,它就会出错:
btoa("héllo ✓");
// InvalidCharacterError: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.
更糟的是,能放进 Latin-1 的字符编码时不会报错,但得到的是错误的字节。btoa("héllo") 返回 aOlsbG8=(Latin-1 的 é = 0xE9),而其他所有语言以及 Base64 编码器 生成的都是 UTF-8 版本 aMOpbGxv。
Node.js 16 及以上版本也以遗留全局函数的形式提供 btoa 和 atob,但在服务器端,Buffer API 更快,还内置了 base64url 编码。见 Node.js 中的 Base64。
编码和解码 UTF-8 字符串
可移植的解决办法是:先用 TextEncoder 把字符串转成 UTF-8 字节,再把这些字节作为“二进制字符串”交给 btoa:
function encodeUtf8(str) {
const bytes = new TextEncoder().encode(str);
let bin = "";
for (const b of bytes) bin += String.fromCharCode(b);
return btoa(bin);
}
function decodeUtf8(b64) {
const bin = atob(b64);
const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
encodeUtf8("héllo wörld"); // "aMOpbGxvIHfDtnJsZA=="
decodeUtf8("aMOpbGxvIHfDtnJsZA=="); // "héllo wörld"
atob("aMOpbGxvIHfDtnJsZA=="); // "héllo wörld" (no TextDecoder)
老式的 btoa(unescape(encodeURIComponent(str))) 技巧结果相同,但依赖已经废弃的 unescape。
Uint8Array.toBase64 与 fromBase64
较新的 API 直接处理字节,不需要二进制字符串这一步。Chrome 和 Edge 140+、Firefox 133+ 以及 Safari 18.2+ 均已支持。
const bytes = new TextEncoder().encode("héllo wörld");
bytes.toBase64(); // "aMOpbGxvIHfDtnJsZA=="
new TextDecoder().decode(Uint8Array.fromBase64("aMOpbGxvIHfDtnJsZA=="));
// "héllo wörld"
还有一个 setFromBase64,它解码到已有的缓冲区中,并报告用掉了多少:
const target = new Uint8Array(8);
target.setFromBase64("aGVsbG8K"); // { read: 8, written: 6 }
使用前先做特性检测:typeof Uint8Array.prototype.toBase64 === "function"。Uint8Array.toBase64 指南 讲解了所有选项。
带填充和不带填充的 URL 安全 Base64
使用新 API 时,传入选项即可:
const d = new TextEncoder().encode("<<???>>");
d.toBase64({ alphabet: "base64url" }); // "PDw_Pz8-Pg=="
d.toBase64({ alphabet: "base64url", omitPadding: true }); // "PDw_Pz8-Pg"
Uint8Array.fromBase64("PDw_Pz8-Pg", { alphabet: "base64url" }); // bytes of "<<???>>"
使用 btoa/atob 时,需要自己转换字母表:
const toB64Url = b64 =>
b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const fromB64Url = s => {
s = s.replace(/-/g, "+").replace(/_/g, "/");
return s + "=".repeat((4 - s.length % 4) % 4);
};
toB64Url(btoa("<<???>>")); // "PDw_Pz8-Pg"
atob(fromB64Url("PDw_Pz8-Pg")); // "<<???>>"
JWT 用的就是这种格式;见 Base64 与 Base64URL 的区别,或者把令牌粘贴到 JWT 解码器 中。
文件、Blob 与大数据
要读取用户选择的文件,FileReader.readAsDataURL 会返回一个 Data URI。去掉逗号及其之前的内容,就是纯 Base64:
const reader = new FileReader();
reader.onload = () => {
const dataUrl = reader.result; // "data:text/plain;base64,aGVsbG8K"
const b64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
};
reader.readAsDataURL(file);
用新 API 写法更短:new Uint8Array(await file.arrayBuffer()).toBase64()。
要把 Base64 还原成可下载的文件,先解码成字节,再用 Blob 包装:
const bytes = Uint8Array.fromBase64(b64);
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
处理大缓冲区时,千万不要把它展开传给 String.fromCharCode(...bytes)。在 500,000 个字节时,Chromium 就会抛出 RangeError: Maximum call stack size exceeded。应该分片构造二进制字符串:
function bytesToBase64(bytes) {
let bin = "";
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// 5,000,000 bytes -> 6,666,668 characters
输出比输入大约三分之一;精确公式见 体积开销指南。
校验与报错信息
atob 会忽略 ASCII 空白并容忍缺失的填充,所以 atob(" aG k= ") 和 atob("aGk") 都返回 "hi"。它会拒绝标准字母表之外的字符:
atob("PDw_Pz8-Pg");
// InvalidCharacterError: Failed to execute 'atob' on 'Window':
// The string to be decoded is not correctly encoded.
fromBase64 抛出的是 SyntaxError,并且可以通过 lastChunkHandling 变得更严格:
Uint8Array.fromBase64("PDw_Pz8-Pg");
// SyntaxError: Found a character that cannot be part of a valid base64 string.
Uint8Array.fromBase64("aGl="); // 2 bytes, accepted
Uint8Array.fromBase64("aGl=", { lastChunkHandling: "strict" });
// SyntaxError: The base64 input terminates with non-zero padding bits.
默认值 "loose" 接受缺失的填充。"strict" 要求填充正确且填充位为零;"stop-before-partial" 会把不完整的最后一块留给流式解码器处理。各浏览器的报错文本不同,但错误名称是标准的。想直观地检查,Base64 校验工具 会高亮确切的问题,Base64 常见错误 列出了各种原因。
常见问题
为什么 btoa 会抛出 InvalidCharacterError?
btoa 只接受 Latin-1 范围内的字符(码位 0–255)。只要有一个 emoji、西里尔字母或中日韩字符,它就会报错。请先用 TextEncoder 把字符串转成 UTF-8 字节,或者使用 Uint8Array.prototype.toBase64()。
Uint8Array.toBase64 能在生产环境中使用吗?
当前版本的 Chrome、Edge、Firefox 和 Safari 都已支持,但旧版浏览器和 Node.js 24 不支持。请用 typeof Uint8Array.prototype.toBase64 === "function" 做特性检测,并回退到基于 btoa 的辅助函数。详见 Uint8Array.toBase64 详解。
为什么 atob 解码后出现 é 这样的乱码,而不是 é?
atob 每个字节返回一个字符,所以一个两字节的 UTF-8 序列会变成两个 Latin-1 字符。把结果经过 Uint8Array.from(bin, c => c.charCodeAt(0)) 和 TextDecoder 处理,就能得到正确的文本。
如何用 atob 解码 Base64URL?
把 - 换成 +、_ 换成 /,再用 = 把长度补齐到 4 的倍数。使用新 API 的话,直接调用 Uint8Array.fromBase64(s, { alphabet: "base64url" }),两步都可以省掉。