上传文件
  1. base64hub
  2. 在代码中使用 Base64
  3. JavaScript
代码

JavaScript 中的 Base64

浏览器提供了只能处理 Latin-1 的 btoa 和 atob,以及较新的、能正确处理字节的 Uint8Array.toBase64() / fromBase64()。下面的输出均来自无头 Chromium。

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" }),两步都可以省掉。

继续阅读