JavaScriptでテキストをBase64に変換する方法(およびデコード方法)
正しく動作する一行コード、エラーを投げるコード、その理由 — さらにUTF-8対応の完全な変換サイクル、2025年のネイティブメソッド、そしてNode.jsの違いについて解説します。
JavaScriptで文字列をBase64エンコードするには、btoa('Hello, world!')を呼び出し、'SGVsbG8sIHdvcmxkIQ=='を返します。この一行コードはASCIIおよびLatin-1テキストにのみ有効ですが、絵文字、アクセント付き文字、または中国語文字を渡すと、すぐにInvalidCharacterErrorを投げます。このガイドでは、なぜそのようなことが起きるのか、そして2025年の新しいネイティブメソッドを含む、これを修正するUTF-8対応の完全な変換サイクルを紹介します。
Base64を一行で:btoa()とatob()
最速の答え:エンコードは btoa('Hello, world!')、デコードは atob('SGVsbG8sIHdvcmxkIQ==') を使います。どちらも検証済みです:btoa('a') は 'YQ==' を返し、atob('SA==') は 'S' を返します。
btoa() が実際にやっていることは、文字列を「バイナリ文字列」として扱い、各文字がバイナリデータの1バイトに相当する文字列として Base64 エンコードすることです。MDNでの使用例は、ASCII制御文字(0〜31)など、テキスト専用のチャンネルを通して運びたいデータです。
テキストがASCIIの場合、この一行で十分です。すべての文字のコードポイントは256未満である必要があり、MDNによると、バイトとコードポイントの対応関係は0x7Fまでしか確実に保たれません。0x80–0xFFの範囲は技術的には動作しますが、これは環境間で移植性のないLatin-1エンコーディングの動作に依存しています。良いルールとして、ASCII以外のものは以下のUTF-8パスを使用してください。
btoa() が絵文字、アクセント記号、CJK文字で InvalidCharacterError を投げる理由
Base64はバイナリエンコーディングであり、テキストエンコーディングではありません。MDNの用語集 は、任意のテキストやHTMLを btoa() に渡す習慣を一般的な誤解と呼んでいます:btoa/atob は型付き配列よりも前に存在するため、バイトを表すために文字列を使用しています。問題は、文字列が1文字1バイトではない場合に始まります。
JavaScriptの文字列はUTF-16なので、コードユニットは常に2バイトであり、BMP外の文字(絵文字など)はサロゲートペアとして4バイトを取ります;UTF-8はコードポイントあたり1〜4バイトを使用します。btoa() と atob() は単一バイト(Latin-1範囲)の文字のみで動作します。それより広いものをエンコードしようとすると、次のような正確なエラーが発生します。
DOMException: Failed to execute 'btoa' on 'Window':
The string to be encoded contains characters outside of the Latin1 range
問題となるケースは簡単に再現できます:btoa('✓') が例外を投げ、btoa('a Ā 𐀀 文 🦄') も同様です。絵文字、Latin-1を超えるアクセント記号、CJK文字はすべて0xFFより上にあるため、1文字1バイトという前提には適合しません。'✓' は U+2713 で、0xFFをはるかに超えているため、btoa() はデータを静かに壊すのではなく拒否します。この拒否は、APIの最も優れた機能と言えるかもしれません:数週間後にモジバケを発見する代わりに、明示的に失敗します。
これを修正する前に知っておくと役立つこと:Base64はデータを生のバイト数より約33%大きくします。短い文字列、トークン、データURIなら問題ありませんが、大規模なペイロード全体をBase64化しないほうがよい本質的な理由にもなります — エンコードは必要な境界で一度だけ行い、あちこちで行わないようにしましょう。

孤立サロゲートのエッジケース(および String.isWellFormed())
多くの記事はこれを省略していますが、後で痛い目を見ます。UTF-16文字列に孤立サロゲート(絵文字ペアの半分、通常はスライシングバグによるもの)が含まれている場合、それは「壊れた(malformed)」状態になります。web.devのガイドによると、TextDecoderはデフォルトでU+FFFDを静かに置換します。エンコードする前に、String.prototype.isWellFormed()でwell-formed(整形式)かどうかを確認するか、孤立サロゲートがあるときにencodeURIComponent()が投げるURIErrorをキャッチし、データを拒否するか、意図的に置換を有効にするか、後の分析のために例外を投げるかを明確に決めてください。
正しい方法:TextEncoder → UTF-8バイト → Base64
正しい修正方法は、まずTextEncoderを使って文字列をUTF-8バイトに変換し、その後、文字列ではなくバイトをBase64エンコードすることです。new TextEncoder().encode(str)は任意のJavaScript文字列をUTF-8バイトのUint8Arrayに変換します(TextEncoderはUTF-8のみを扱います)。復元時は、new TextDecoder()はデフォルトでUTF-8を使用し、正確に逆変換します。ここでatob()が返すものにご注意ください:これはバイナリ文字列であり、1文字が1バイトに対応しますが、UTF-8テキストではありません。そのため、復元側ではTextDecoderを使ってバイトを読み取り可能なテキストに戻す必要があります。
bytesToBase64()およびbase64ToBytes()ヘルパー関数(コピペ用コード)
ここにMDNの標準的な往復処理を示します。これはTextEncoderをサポートするすべてのブラウザ(IEを除く)で動作します。
function bytesToBase64(bytes) {
const binString = Array.from(bytes, (b) => String.fromCodePoint(b)).join('');
return btoa(binString);
}
function base64ToBytes(base64) {
const binString = atob(base64);
return Uint8Array.from(binString, (m) => m.codePointAt(0));
}
完全な往復処理で検証する
文字列のエンコードは1行で完了します。
const encoded = bytesToBase64(new TextEncoder().encode('a Ā 𐀀 文 🦄'));
// 'YSDEgCDwkICAIOaWhyDwn6aE'
デコードはそれを逆に実行します。
const decoded = new TextDecoder().decode(base64ToBytes(encoded));
// 'a Ā 𐀀 文 🦄'
web.devが検証した往復処理も同じパターンを確認しています:'hello⛳❤️🧀'は'aGVsbG/im7PinaTvuI/wn6eA'にエンコードし、絵文字を含めて正確に復元します。
unescape(encodeURIComponent())というトリックは避ける
btoa(unescape(encodeURIComponent(str)))はあちこちでコピーされているのを見かけるでしょう。これは機能しますが、unescape()はAnnex Bのレガシー仕様であり、ECMAScript 3以来非推奨です。新しいコードでは代わりにTextEncoderを使用すべきです。明示的で標準的であり、非推奨のグローバル関数に依存しないためです。
2025年の最新手法:Uint8Array.toBase64()とUint8Array.fromBase64()
2025年9月以降、JavaScriptにはUint8Array自体にネイティブのBase64メソッドが実装されており、上記のバイト文字列ヘルパーだけが選択肢というわけではありません。TC39のBase64提案はStage 4に到達し、言語の一部となりました。TC39の理由は、Base64はテキストエンコーディングではないため、TextEncoder/TextDecoderは概念的に適切なツールではないということです。
MDNによると、これらのメソッドは常に利用可能なブラウザ全体で「新規利用可能」としてベースラインに含まれています。リリース前にターゲットランタイムを確認してください:古いブラウザや古いNodeバージョンをサポートする場合は、TextEncoder + btoaのフォールバックを維持してください。MDNは、すでにUint8Arrayがある場合に、生のバイト文字列を作成する必要がないため、これらのメソッドをbtoa()/atob()よりも優先しています。
エンコード:bytes.toBase64()
const b64 = new TextEncoder().encode('Hello, world!').toBase64();
// 'SGVsbG8sIHdvcmxkIQ=='
UTF-8変換を1回、メソッド呼び出しを1回行うだけで完了です。
デコード:Uint8Array.fromBase64()
const text = new TextDecoder().decode(Uint8Array.fromBase64(b64));
// 'Hello, world!'
MDNのドキュメント fromBase64() は、デコードされたデータがASCIIテキストとして意図されていない限り、実際のバイト配列を返すため、 atob() よりも優先して使用すべきです。これは base64 アルファベット(A–Z、a–z、0–9、および +/ または -_)のみを受け入れ、オプションの = パディングに対応し、ASCIIの空白文字は無視します。アルファベット外の文字が含まれていると、 SyntaxError がスローされます。
オプション: alphabet, omitPadding, lastChunkHandling
仕様のオプションバッグ により、きめ細かな制御が可能です。
| オプション | 値 | 機能 |
|---|---|---|
alphabet |
'base64' (既定値), 'base64url' |
+// と -/_ の比較 |
omitPadding |
boolean | 末尾の = を削除します: new Uint8Array([72]).toBase64() → 'SA=='、omitPadding → 'SA' |
lastChunkHandling |
'loose' (既定値), 'strict', 'stop-before-partial' |
最終チャンクに対するデコーダーの許容度 |
'loose' は、パディング不足や空白を許容します。 'strict' は、オーバーフロービットをゼロにした正確な4文字のチャンクを要求します。これは信頼できない入力を検証する際に望ましい挙動です。 'stop-before-partial' はストリーミング用です。既定の緩いモードでは、厳格モードが拒否する非ゼロのオーバーフロービットも許容されます。
Base64 と base64url: どちらのアルファベットが必要か?
標準的な base64 は、 + と / に加えて = パディングを使用し、パスセグメントやクエリ文字列を持たない data: URL では問題ありません。base64url(RFC 4648 §5)は、 - と _ に置き換えられ、通常はパディングを省略します。出力がURL内に含まれる場合は必ず必要です。例えば、JWTは3つのセグメントすべてに base64url を使用します。
base64url を使用するタイミング
エンコードされた文字列がURLのパスまたはクエリパラメータの一部になる場合、 + や / はエスケープが必要になり、 = パディングはクエリ文字列を煩雑にするため、 base64url を使用してください。データURI、JSONペイロードへのバイト埋め込み、URLではないその他の用途には、標準的な base64 を使用します。
base64url の生成方法
環境に応じて3つの方法があります。
- 最新のブラウザ:
bytes.toBase64({ alphabet: 'base64url' }) - Node 16以降:
Buffer.from(str).toString('base64url') - 後処理: 標準的な出力を取得し、
+→-、/→_に置換してから、=を削除します。
知っておくと役立つ微妙な点: RFC 4648 では、エンコーダーが最終チャンクのデータ以外のオーバーフロービットをゼロにすることが義務付けられており、厳格なデコーダーはそれがゼロでない場合にエラーをスローする可能性があります。MDNの用語集では、 'VR==' の例が示されており、これは 'VQ==' と同じバイトですが、パディングビットが汚れています。動作が保証されたエンコーダーで base64 を生成すればこの問題は発生しません。独自に実装すると発生する可能性があります。
ファイルや画像を Base64(データURI)に変換する方法
ファイルの場合、ブラウザでの方法は、 readAsDataURL() を使用した FileReader で完全なデータURIを作成し、プレフィックスを除去して生の base64 を取得することです。すでにキャンバス上に描画されている画像の場合、 toDataURL('image/png') を1回の呼び出しで実行できます。
FileReader.readAsDataURL() → データURI → 生の base64
function fileToDataURL(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(file);
});
}
const dataUrl = await fileToDataURL(fileInput.files[0]);
const rawBase64 = dataUrl.replace(/^data:\w+\/\w+;base64,/, '');
canvas.toDataURL() (画像用)
キャンバスに画像を描画した(リサイズ、切り抜きなど)場合は、FileReader を完全にスキップしてください。
const dataUrl = canvas.toDataURL('image/png');
生の Base64 文字列のみが必要な場合も、同様にプレフィックスの除去処理が必要です。
Data URL からバイト列への変換
逆方向の変換は、fetch と ArrayBuffer の組み合わせで行います。MDN の例では、new Uint8Array([0,1,2]) から構築された Data URL は 'data:application/octet-stream;base64,AAEC' となり、以下のようにバイト列を復元できます。
const res = await fetch(dataUrl);
const bytes = new Uint8Array(await res.arrayBuffer());
// [0, 1, 2]
そして、この記事が存在する理由に関する重要な注意点:任意のテキストや HTML に対して btoa() を呼び出して Data URL を作成しないでください。これはバイナリ単位でのエンコーダーであり、Latin-1 以外の入力ではエラーが発生します。
Node.js: Buffer.from(str, 'base64') — およびその危険な違い
Node には、TextEncoder を使う面倒な手順を省ける定番の書き方があります。Buffer は最初からバイト単位で動作するため、Buffer.from('Hello, world! 🌍').toString('base64') は Unicode を自動的に処理します。デコードには Buffer.from(encoded, 'base64').toString('utf8') を使用します。
その他の有用な形式:Node 16 以降で使用可能な Buffer.from(str).toString('base64url') や、ファイル全体を対象とする fs.readFileSync('image.png').toString('base64') などがあります。
const encoded = Buffer.from('Hello, world! 🌍').toString('base64');
const decoded = Buffer.from(encoded, 'base64').toString('utf8');
Node は Base64 の検証を行いません(エラーが出ずに黙って失敗します)
ここで人々が驚くポイント:Node の Base64 デコーダーは入力を検証しません。Buffer.from('!!!!', 'base64').toString() はエラーを返さずに '' を返し、Buffer.from('aGVsbG8!!!', 'base64') は不正なデータを黙って破棄して 'hello' を返します。これに対し、atob('aGVsbG8!!!') は InvalidCharacterError をスローします。Base64 がユーザー、API、またはサードパーティから提供される場合は、Node でデコードする前に検証するか、ランタイムがサポートしている場合は Uint8Array.fromBase64() に lastChunkHandling: 'strict' を組み合わせて使用してください。
btoa/atob は非推奨ですか?ブラウザと Node の違い
これは確かに矛盾しているように見えるため、正直な解決策を提示します。ブラウザでは、MDN が btoa() と atob() Baseline は 2015 年 7 月以来広く利用可能であり、非推奨ではありません。一方、Node では 型定義 でそれらが @deprecated とマークされ、「新しいコードでは決して使用すべきではない」というコメントと共に、代わりに Buffer.from(str, 'base64') を推奨しています。
良いルールとして、これらは削除された API ではなくレガシー API です。ブラウザコードでは問題ありませんが、Node 固有のコードでは Buffer を優先してください。コミュニティの合意(仕様上の保証ではありませんが)、依存関係なしでブラウザ、Deno、Node で変更せずに実行できる単一のコードパスが必要な場合、btoa/atob が実用的なクロス環境の選択となります。
実際に何を使用すべきか
この記事から一つの推奨事項を選ぶなら、以下の 3 つのワンライナーです。プレーンな ASCII やレガシーなコードベースでは btoa/atob を維持し、bytesToBase64 ヘルパーをブラウザのフォールバックとして使用してください。これは、toBase64() があなたの全オーディエンスをカバーするまで有効です。
// Modern browsers (2025+)
const enc = new TextEncoder().encode(str).toBase64();
const dec = new TextDecoder().decode(Uint8Array.fromBase64(b64));
// Browser fallback (MDN helper)
const encFallback = bytesToBase64(new TextEncoder().encode(str));
// Node (validate input first if it isn't yours)
const encNode = Buffer.from(str).toString('base64');
リリース前に、ブラウザツールで簡単な往復テストを行い、出力を確認してください。'Hello, world!' を Tool Cloud の無料 Base64 エンコーダー / デコーダー に貼り付け、'SGVsbG8sIHdvcmxkIQ==' が得られることを確認してから、再度デコードして同一であることを確認してください。また、スタンドアロンの テキストから Base64 への変換 や Base64 からテキストへの変換 も用意されています。ファイルは自動的に削除され、インストールは不要です。
よくある質問
なぜbtoa()は絵文字では失敗するのに、英語テキストでは正常に動作するのでしょうか?
btoa()は各文字を1バイトとして扱い、各コードポイントが256未満(確実に0x7Fまで)であることを要求します。絵文字、アクセント付き文字、CJK(中国語・日本語・韓国語)テキストは複数のバイトが必要なため、DOMException: InvalidCharacterErrorが発生します。まずTextEncoderを使ってUTF-8バイトに変換し、その後エンコードしてください。
btoa()は非推奨になっていますか?
ブラウザ上では非推奨ではありません。MDNによると、btoa()とatob()は2015年7月以降広く利用可能(Baseline widely available)です。ただし、Node.jsの型定義では、Node固有のコードにおいてBuffer.from(str, 'base64')を推奨し、それらを非推奨としています。これらは削除されたAPIではなく、レガシーAPIです。
ユニコードテキスト(アクセント、中国語、絵文字など)をBase64エンコードするにはどうすればよいですか?
new TextEncoder().encode(str)を使用してUTF-8バイトを取得し、その後バイト列をエンコードします(ブラウザではbtoa()をバイト列ヘルパー経由で使用するか、Uint8Array.toBase64()を使用します)。デコードするには、Uint8Array.fromBase64()またはatob()でバイト列に戻し、その後new TextDecoder().decode(bytes)を実行します。
base64とbase64urlの違いは何ですか?
base64url(RFC 4648 §5)は'+'と'/'の代わりに'-'と'_'を使用し、通常'='パディングを省略します。出力結果がURLやJWTに含まれる場合はbase64urlを使用し、パスセグメントやクエリ文字列を持たないdata: URLには標準的なbase64で問題ありません。
なぜNode.jsは不正なBase64に対してエラーを投げないのですか?
NodeのBuffer.from(str, 'base64')は入力検証を行わず、無効な文字を静かに無視するため、Buffer.from('aGVsbG8!!!', 'base64').toString()はエラーなしで'hello'を返します。一方、ブラウザではatob()が同じ入力でInvalidCharacterErrorを投げます。Nodeで信頼できないBase64をデコードする前に、必ず検証を行ってください。