[JS] 10 min read

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

JavaScript 中 Base64 的實務指南。何時該用 btoa/atob、為何它們在 Unicode 上出錯、Buffer 在 Node.js 中的運作方式,以及跨執行環境的 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 —— 這是在做 Base64 編碼之前把 Unicode 字串轉成位元組的標準做法。知道該伸手拿哪個工具,勝負就已經分了一半。

// 瀏覽器: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 的程式碼單元當成一個位元組來讀。

解決方法是先把 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 安全輔助函式。

// 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 安全:Buffer 將 'base64url' 視為一等公民的編碼(Node 16+);瀏覽器需要自訂正規表示式替換。
  • > 效能:Buffer 以 C++ 實作,在大型輸入上比 atob/btoa 快約 3–5 倍。
  • > 串流:Buffer 可搭配 transform stream 處理大型檔案;btoa/atob 無法串流。
  • > 可用性:btoa/atob 在 Node 16+ 也以全域函式存在,但有選擇時請優先用 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 安全 BASE64 (BASE64URL)

標準 Base64 使用 +/,兩者在 URL 中都是保留字元。它結尾還會有一或兩個 =,會被百分比編碼成 %3D,浪費空間。RFC 4648 §5 定義了 URL 安全變體(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 的多位元組序列會以亂碼呈現。
  • > 'Maximum call stack size exceeded' —— 你在超過約 65,000 個位元組的陣列上使用了 String.fromCharCode(...bytes)。修正:分塊處理(參考前面 32 KB 的迴圈範例)。
  • > 瀏覽器與 Node 輸出不同 —— 通常代表其中一邊是 URL 安全,另一邊是標準。在介面邊界統一成同一種變體。
  • > 重複編碼的內容 —— 資料被 Base64 兩次。檢查:atob(input) 看起來是不是又像 Base64?那就再解一次。
  • > JSON 中的 Base64 被意外再 JSON 編碼 —— 你把 Base64 字串用 JSON.stringify 包起來,接收端解析 JSON 後得到的字串不變,但你的伺服器程式碼卻誤以為需要再編一次。檢查傳輸線上的實際內容。

// 跨執行環境輔助函式(瀏覽器 + 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()
  • > 避免反覆的編碼/解碼週期 —— 每一次往返在時間與記憶體上都是 O(n)。若要多次使用,請把 Base64 形式快取起來。
  • > 串流 —— 對真正的巨量資料(100 MB+),請使用 Node 的 stream.Transform 或瀏覽器的 TransformStream API,搭配分塊式的 Base64 編碼器。

// 延伸閱讀

什麼是 Base64?它是如何運作的? —— 從頭說清楚 6-bit 分組技巧。
Base64 與 UTF-8、Unicode —— 深入探討 TextEncoder 的操作。
URL 安全版與標準版 Base64 —— 何時需要 -_ 而不是 +/
試用即時 Base64 編碼器 —— 貼上文字、查看編碼、複製結果。

若你經常要呼叫 btoa(),請把本頁加入書籤——十次中有九次,你正要 Google 的問題都在這裡。