一行代码完成编码和解码
这个 API 新增了三个 Base64 方法和三个 hex 方法:
| 方法 | 作用 |
|---|---|
bytes.toBase64(options) | Uint8Array 转 Base64 字符串 |
Uint8Array.fromBase64(string, options) | Base64 字符串转新的 Uint8Array |
bytes.setFromBase64(string, options) | 解码到已有的 Uint8Array 中 |
bytes.toHex() | Uint8Array 转小写 hex 字符串 |
Uint8Array.fromHex(string) | hex 字符串转新的 Uint8Array |
bytes.setFromHex(string) | 把 hex 解码到已有的 Uint8Array 中 |
文本必须先变成字节。TextEncoder 总是输出 UTF-8,所以处理 Unicode 不需要任何技巧:
const bytes = new TextEncoder().encode("Grüße, 世界");
const b64 = bytes.toBase64();
console.log(b64); // "R3LDvMOfZSwg5LiW55WM"
const back = new TextDecoder().decode(Uint8Array.fromBase64(b64));
console.log(back); // "Grüße, 世界"
ArrayBuffer(例如来自 fetch().arrayBuffer() 或 crypto.subtle.digest())需要先套一个视图:new Uint8Array(buffer).toBase64()。其他类型化数组根本没有这些方法。要编码 Uint16Array 的原始内存,请这样包装:new Uint8Array(u16.buffer, u16.byteOffset, u16.byteLength)。
选项:alphabet、omitPadding、lastChunkHandling
alphabet
"base64"(默认值)使用 + 和 /,"base64url" 使用 - 和 _。这个选项对编码和解码都有效,设为某一种字母表的解码器会拒绝另一种字母表的字符。
const data = new Uint8Array([251, 255]);
data.toBase64(); // "+/8="
data.toBase64({ alphabet: "base64url" }); // "-_8="
data.toBase64({ alphabet: "base64url", omitPadding: true }); // "-_8"
Uint8Array.fromBase64("-_8", { alphabet: "base64url" }); // Uint8Array [251, 255]
Uint8Array.fromBase64("-_8"); // SyntaxError
omitPadding
toBase64 默认会添加 = 填充,即使使用 Base64URL 字母表也是如此。要得到 JWT 风格的输出,请传入 omitPadding: true。解码时的填充由 lastChunkHandling 控制。
lastChunkHandling
这个选项决定 fromBase64 和 setFromBase64 如何处理最后一组字符。下面用解码结果为 Hi?> 字节的输入测试三种模式:
| 输入 | "loose"(默认) | "strict" | "stop-before-partial" |
|---|---|---|---|
SGk/Pg== | Hi?> | Hi?> | Hi?> |
SGk/Pg(无填充) | Hi?> | SyntaxError | Hi?(在 Pg 之前停止) |
SGk/Ph==(填充比特非零) | Hi?> | SyntaxError | Hi?> |
SGk/P(剩下一个字符) | SyntaxError | SyntaxError | Hi? |
- loose 的行为与
atob相同:填充可有可无,最后一个字符中多余的比特会被忽略。 - strict 只接受 RFC 4648 所描述的规范、带填充的输入。当编码形式必须唯一时使用它,例如需要比较或签名 Base64 字符串的场景。
- stop-before-partial 会解码所有完整的 4 字符分组,不处理不完整的尾部。它用于流式处理,即分组的剩余部分会在下一个数据块中到达。
在所有模式下,ASCII 空白(空格、制表符、换行符)都会被跳过,所以 MIME 折行的输入可以直接解码。字母表以外的其他任何字符都会抛出 SyntaxError。
写入已有的缓冲区
setFromBase64 和 setFromHex 会解码到你已经准备好的 Uint8Array 中,并返回用掉了多少:
const target = new Uint8Array(8);
const result = target.setFromBase64("SGVsbG8gd29ybGQ=");
console.log(result); // { read: 8, written: 6 }
Hello world 有 11 个字节,而目标只能容纳 8 个。解码器只写入放得下的完整 3 字节分组,所以它在 6 个字节后停止,并报告读取了 8 个字符。用字符串的剩余部分和目标的 subarray 再次调用即可继续。配合 stop-before-partial,就能把数据流逐块解码到预先分配好的缓冲区中:
const out = new Uint8Array(1024);
const { read, written } = out.setFromBase64("SGk/Pg", {
lastChunkHandling: "stop-before-partial",
});
// read: 4, written: 3. Keep "Pg" and prepend it to the next chunk.
hex 方法遵循同样的模式。toHex() 总是输出小写,fromHex() 接受大小写两种形式,数字个数为奇数时会抛出 SyntaxError:
new Uint8Array([0xde, 0xad, 0xbe, 0xef]).toHex(); // "deadbeef"
Uint8Array.fromHex("CAFE"); // Uint8Array [202, 254]
为什么 btoa 和 atob 不好用
btoa 和 atob 诞生于浏览器的早期,处理的是“二进制字符串”:每个字符编码都是 0 到 255 之间字节值的 JavaScript 字符串。这带来了三个问题。
Unicode 要么报错,要么悄悄出错。U+00FF 以上的字符会让 btoa 抛出 InvalidCharacterError(Chrome:“The string to be encoded contains characters outside of the Latin1 range”)。U+0080 到 U+00FF 之间的字符更糟:它们会以 Latin-1 编码,不报任何错误。btoa("Grüße") 返回 R3L832U=,而其他所有系统期望的 UTF-8 编码是 R3LDvMOfZQ==。
解码返回的是字符串,而不是字节。atob("Q2Fmw6k=") 返回 "Café"。要得到真正的文本,得先把字符串转成字节,再按 UTF-8 解码,这需要写一个循环,或者 Uint8Array.from(atob(s), c => c.charCodeAt(0))。
处理大输入需要小心。常见的一行写法 btoa(String.fromCharCode(...bytes)) 会把每个字节都作为函数参数传入,遇到 1 MB 的数组就会抛出 RangeError: Maximum call stack size exceeded。你必须分块构建字符串。
此外,它们不支持 Base64URL,也无法控制填充或严格程度。老办法 btoa(unescape(encodeURIComponent(text))) 确实能得到 UTF-8 的 Base64,但依赖已被弃用的 unescape。有了新 API,这一切都变成了 new TextEncoder().encode(text).toBase64()。
浏览器与 Node.js 支持情况
这些方法来自 TC39 提案“Uint8Array to/from Base64”,该提案于 2025 年 7 月进入 Stage 4,属于 ECMAScript 2026 的一部分。
| 运行时 | 首个支持全部六个方法的版本 |
|---|---|
| Chrome、Edge | 140 |
| Firefox | 133 |
| Safari(macOS 和 iOS) | 18.2 |
| Node.js | 25.0.0 |
随着 Chrome 140 发布,该特性于 2025 年 9 月成为 Baseline,所有当前的主流浏览器都已支持。在 Node.js 中,我们直接测试了这些方法。Node 25.0.0 和 26.12.1 拥有全部六个方法。LTS 版本 Node 24.22.1 默认一个都没有。它的 V8 接受实验性的 --js-base-64 参数,但在我们的测试中,这个参数只开放了 toBase64、fromBase64、toHex 和 fromHex,没有 setFromBase64 和 setFromHex,所以应当视 Node 24 为不支持。在 Node 24 及更早版本上,请使用 Buffer.from(b64, "base64") 和 buf.toString("base64"),它们也接受 "base64url"。根据 MDN 的兼容性数据,Deno 2.5 和 Bun 1.1.22 同样支持这个 API。
兼容旧运行时的写法
先做特性检测,不支持时回退到分块处理的 btoa/atob。这个版本已在 Node 24(回退路径)和 Node 26(原生路径)上用 UTF-8 文本和 1 MB 数组测试过:
function bytesToBase64(bytes) {
if (typeof bytes.toBase64 === "function") return bytes.toBase64();
let bin = "";
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
function base64ToBytes(b64) {
if (typeof Uint8Array.fromBase64 === "function") return Uint8Array.fromBase64(b64);
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
}
这个回退只覆盖默认选项。如果你需要在所有环境中使用 base64url、strict 或 setFromBase64,请使用完整的 polyfill:core-js 提供了 es.uint8-array.to-base64、es.uint8-array.from-base64 及相关模块。
不想写代码也能试试各种编码:使用 Base64 编码器和解码器,URL 安全字母表则用 Base64URL。更多 JavaScript 示例(包括 Node 的 Buffer)见 JavaScript 中的 Base64 和 Node.js 中的 Base64。
常见问题
这个新的 Base64 API 现在可以在生产环境中使用吗?
在浏览器中可以:Chrome 和 Edge 140、Firefox 133、Safari 18.2 都已支持。Node.js 需要 25 或更高版本。如果要支持更老的浏览器或 Node 24 及以下版本,请保留兼容写法。
Uint8Array.fromBase64 支持 Base64URL 吗?
支持,传入 { alphabet: "base64url" } 即可。不加这个选项时,- 和 _ 会导致 SyntaxError。在默认的宽松模式下,两种字母表的填充都是可选的。
怎样用 toBase64 编码字符串?
先把它转成 UTF-8 字节:new TextEncoder().encode(text).toBase64()。解码时反过来:new TextDecoder().decode(Uint8Array.fromBase64(b64))。
为什么我的 Buffer 或 Uint16Array 上没有 toBase64 方法?
这些方法只存在于 Uint8Array 及其子类上。Node 的 Buffer 从 Node 25 起才有。对于其他类型化数组,请包装底层内存:new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength)。