[JS] 10 min read

[JS] JavaScript 中的 Base64——atob、btoa 与 Node Buffer

一份面向工程实战的 JavaScript Base64 指南。btoa/atob 什么时候用、为什么遇到 Unicode 就挂、Node.js 的 Buffer 如何工作,以及一个跨运行时的 base64url 辅助函数。

April 2026 | javascript

// 全景

JavaScript 与 Base64 的关系被历史切得支离破碎。浏览器从 1990 年代 Netscape 那里继承了 btoa() / atob()——这两个名字字面意思是 binary-to-ASCIIASCII-to-binary。它们只能处理 Latin-1 范围内的字符,这也是为什么你把 emoji 或中文字符复制进 btoa() 就会直接抛错。

Node.js 则通过 Buffer 类走了自己的路。它支持几乎所有你会用到的编码(utf8、base64、base64url、hex、latin1、ascii),能原生处理二进制数据,是服务端代码里显而易见的首选。

现代运行时(Node 18+、Deno、Bun、现代浏览器)还提供了 TextEncoder / TextDecoder——这是标准化的办法,把 Unicode 字符串先转成字节再做 Base64 编码。知道在哪种场景下该选哪种工具,就已经成功了一半。

// 浏览器:btoa / atob——正确用法

btoa(str) 接受一个字符串,其中每个字符必须在 1 个字节内(Latin-1 范围,U+0000 到 U+00FF),并返回一个 Base64 字符串。atob(b64) 是其逆操作。它们是同步的、快速的,并且自古以来每个浏览器都内置。

它们的正确用法是:对已经基本是字节形态的数据做 Base64。比如从 FileReader 读出的原始字节、从二进制 XMLHttpRequest(responseType: 'arraybuffer')里得到的字节、或 Uint8Array 这类每个字节数值都在 0–255 之间的数据。

// Encode a Latin-1 string
const latin1 = 'Hello, world!';
const encoded = btoa(latin1);   // 'SGVsbG8sIHdvcmxkIQ=='

// Decode it back
const decoded = atob(encoded);  // 'Hello, world!'

// Encode a binary Uint8Array (the common real-world case)
const bytes = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0]);  // JPEG magic
const binStr = String.fromCharCode(...bytes);            // 'ÿØÿà' (Latin-1)
const b64 = btoa(binStr);                                // '/9j/4A=='

// 为什么 btoa() 遇到 UNICODE 就抛错

在浏览器控制台里试 btoa('héllo')。你会得到 InvalidCharacterError: The string to be encoded contains characters outside of the Latin1 rangeé(U+00E9)没问题,但 btoa('日本語') 会挂,因为 (U+65E5)超出了 Latin-1。这个函数根本没有 UTF-8 的概念;它只是把 JavaScript String 里的每一个 code unit 当作一个字节来读。

解决办法是:先把 Unicode 字符串转为 UTF-8 字节,再对这些字节做 Base64。任何现代浏览器都内置了 TextEncoder 来干这件事。

// ❌ broken
// btoa('日本語')  → throws InvalidCharacterError

// ✅ correct: encode to UTF-8 first
function b64encodeUtf8(str) {
  const bytes = new TextEncoder().encode(str);
  let bin = '';
  for (const byte of bytes) bin += String.fromCharCode(byte);
  return btoa(bin);
}

b64encodeUtf8('日本語');   // '5pel5pys6Kqe'

// ✅ round-trip decode
function b64decodeUtf8(b64) {
  const bin = atob(b64);
  const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

b64decodeUtf8('5pel5pys6Kqe');  // '日本語'

// NODE.JS:BUFFER 套路

在 Node 里请优先用 Buffer。它默认感知 UTF-8,支持市面上一切编码,并且原生地处理二进制。Node 16+ 还识别 'base64url' 作为一种编码名,因此做 URL-safe 输出不需要再额外写辅助函数。

// String → Base64
const encoded = Buffer.from('héllo 世界').toString('base64');
// 'aMOpbGxvIOS4lueVjA=='

// Base64 → string (UTF-8 by default)
const decoded = Buffer.from(encoded, 'base64').toString();
// 'héllo 世界'

// URL-safe (Node 16+): +/- omitted padding
const safe = Buffer.from('héllo 世界').toString('base64url');
// 'aMOpbGxvIOS4lueVjA'  ← no = padding, uses -_

// Binary file → Base64
const fs = require('node:fs');
const imgB64 = fs.readFileSync('logo.png').toString('base64');

// Base64 → binary file
fs.writeFileSync('out.png', Buffer.from(imgB64, 'base64'));

// BUFFER 与 btoa/atob 的快速对比

  • > Unicode:Buffer 原生感知 UTF-8;btoa/atob 只认 Latin-1 字节。
  • > 二进制:Buffer 是 Node 的原生二进制类型;浏览器必须借助 Uint8Array + String.fromCharCode。
  • > URL-safe:Buffer 把 'base64url' 作为头等编码支持(Node 16+);浏览器需要自己写正则替换。
  • > 性能:Buffer 由 C++ 实现,在大输入上比 atob/btoa 快 3–5 倍。
  • > 流式:Buffer 可以接到 transform stream 里处理大文件;btoa/atob 无法流式处理。
  • > 可用性:Node 16+ 里 btoa/atob 也是全局,但能选的时候优先选 Buffer——它能覆盖边界情况。

// 在浏览器里编/解码二进制文件

浏览器里把文件选择器选中的文件转成 Base64 字符串的经典套路是 FileReader.readAsDataURL(),它直接返回一个可用的 data URI。如果你只要纯 Base64 载荷,就把 data:...;base64, 前缀切掉。

对于超大文件,改用 readAsArrayBuffer() 读取并分块编码,以避开 String.fromCharCode(...bytes) 在超过 ~65,000 个元素时遇到的 'maximum call stack size exceeded' 错误。

// ▸ Simple: file → data URI → strip prefix
async function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const dataUri = reader.result;                      // 'data:image/png;base64,iVBOR...'
      resolve(dataUri.split(',')[1]);                     // raw Base64 only
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

// ▸ Large files: chunked btoa() to avoid stack overflow
async function arrayBufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  const chunkSize = 0x8000;      // 32 KB chunks
  let binary = '';
  for (let i = 0; i < bytes.length; i += chunkSize) {
    binary += String.fromCharCode.apply(null,
      bytes.subarray(i, i + chunkSize));
  }
  return btoa(binary);
}

// Usage
const file = inputEl.files[0];
const buf = await file.arrayBuffer();
const b64 = await arrayBufferToBase64(buf);

// 在浏览器里把 BASE64 解码回字节

// ▸ Base64 → Uint8Array (binary bytes)
function base64ToBytes(b64) {
  const bin = atob(b64);
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return bytes;
}

// ▸ Base64 → Blob → downloadable
function base64ToBlob(b64, mime = 'application/octet-stream') {
  const bytes = base64ToBytes(b64);
  return new Blob([bytes], { type: mime });
}

// Trigger a download
const blob = base64ToBlob(b64, 'image/png');
const url = URL.createObjectURL(blob);
Object.assign(document.createElement('a'), { href: url, download: 'out.png' }).click();
URL.revokeObjectURL(url);

// JavaScript 中的 URL-SAFE BASE64(base64url)

标准 Base64 用到 +/,这两个在 URL 里都是保留字符。末尾一个或两个 = 还会被百分号编码成 %3D,白白浪费空间。RFC 4648 §5 定义了 URL-safe 变体(base64url):+ 换成 -/ 换成 _,并且不保留填充。

在 Node 16+ 上,直接用 Buffer.from(x).toString('base64url')。在浏览器里,则在 btoa() 之上做一层后处理。

// ▸ Browser: standard Base64 → URL-safe Base64
function toBase64Url(b64) {
  return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// ▸ URL-safe Base64 → standard Base64 (before feeding atob)
function fromBase64Url(b64u) {
  let b64 = b64u.replace(/-/g, '+').replace(/_/g, '/');
  while (b64.length % 4) b64 += '=';   // re-add padding
  return b64;
}

// Round-trip
const token = toBase64Url(btoa('hello+world/'));
// 'aGVsbG8rd29ybGQv'
atob(fromBase64Url(token));  // 'hello+world/'

// 常见坑与如何排查

  • > btoa() 抛出 InvalidCharacterError——你的输入含有 Latin-1 之外的字符。修复:先用 TextEncoder 转成 UTF-8 字节。
  • > atob() 抛出 InvalidCharacterError——输入不是合法的 Base64,通常是 base64url 少了填充,或是混入了空白字符。去掉空白、补回 = 填充。
  • > 解码后 Unicode 乱码——你直接用 atob() 解码而没经过 TextDecoder。UTF-8 的多字节序列会变成乱码(mojibake)。
  • > 'Maximum call stack size exceeded'——你对一个超过 ~65,000 字节的数组调用了 String.fromCharCode(...bytes)。修复:按 32 KB 分块(见上文循环)。
  • > 浏览器和 Node 输出不一致——通常意味着一边是 URL-safe、另一边是标准。应在交界处统一到同一个变体。
  • > 载荷被重复编码——数据被 Base64 了两次。检查:atob(input) 结果看起来是否又像 Base64?如果是,再解一次。
  • > JSON 中的 Base64 被多次 JSON 编码——你把 Base64 字符串丢进 JSON.stringify,接收方解析 JSON 拿回原字符串,但你的服务端代码又额外编了一次。抓一下线上 payload 看看。

// 跨运行时辅助函数(浏览器 + NODE + DENO + BUN)

// A portable Base64/base64url helper. Uses Buffer when available,
// falls back to btoa/atob + TextEncoder in the browser / Deno.

const hasBuffer = typeof Buffer !== 'undefined';

export function encode(str, { urlSafe = false } = {}) {
  if (hasBuffer) {
    return Buffer.from(str, 'utf8').toString(urlSafe ? 'base64url' : 'base64');
  }
  const bytes = new TextEncoder().encode(str);
  let bin = '';
  for (const b of bytes) bin += String.fromCharCode(b);
  const b64 = btoa(bin);
  return urlSafe
    ? b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
    : b64;
}

export function decode(b64, { urlSafe = false } = {}) {
  if (hasBuffer) {
    return Buffer.from(b64, urlSafe ? 'base64url' : 'base64').toString('utf8');
  }
  if (urlSafe) {
    b64 = b64.replace(/-/g, '+').replace(/_/g, '/');
    while (b64.length % 4) b64 += '=';
  }
  const bin = atob(b64);
  const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

// Usage
encode('héllo 世界');                      // 'aMOpbGxvIOS4lueVjA=='
encode('héllo 世界', { urlSafe: true });    // 'aMOpbGxvIOS4lueVjA'
decode('aMOpbGxvIOS4lueVjA==');            // 'héllo 世界'

// 性能笔记

  • > Buffer 最快——Node 里它跑在 C++,处理 MB 级输入也不抖。
  • > btoa/atob 在小载荷(< 100 KB)上有一定竞争力,但它们是同步的,大输入会阻塞主线程。
  • > 浏览器里处理 > 10 MB 的输入,请用 Worker,把编码放到主线程之外——或者使用 Blob.text() + FileReader.readAsDataURL(),这两个 API 内部做了优化。
  • > 避免反复编解码——每一次往返在时间和内存上都是 O(n)。如果会多次使用,缓存其 Base64 形式。
  • > 流式——对于真正超大的载荷(100 MB+),用 Node 的 stream.Transform 或浏览器的 TransformStream API 配合一个分块 Base64 编码器。

// 相关阅读

Base64 是什么?它是如何工作的?——从零讲清 6-bit 分组。
用 Base64 处理 UTF-8 与 Unicode——TextEncoder 的完整舞步。
URL-safe 与标准 Base64 的差别——什么时候需要 -_ 而不是 +/
在线试一下 Base64 编码器——粘贴文本,看编码结果,复制。

如果你经常伸手去按 btoa(),把这一页收藏起来——十次里有九次你想 Google 的答案就在这里。