encodeURIとencodeURIComponentの違い:どちらを使うべきか

encodeURIComponentは、encodeURIがそのまま残す11文字をエンコードします。この11文字が、値がパラメーターの中にとどまるかどうかを決めます。

JavaScriptにはパーセントエンコーディング用の組み込み関数が2つあり、違いはただ1点、URLの構文をどこまでそのまま残すかです。encodeURIは完全なURLを受け取る前提で、/、?、&のようにURLの構造を形作る文字を残します。encodeURIComponentはデータの断片を受け取る前提で、それらの文字もエンコードし、データが構造と取り違えられないようにします。要するに、URLに値を入れるならencodeURIComponentを、できればURL APIを使ってください。

正確な違い

どちらの関数も、そのまま残さない文字をすべてUTF-8に変換し、各バイトを%と大文字の16進数2桁で書き表します。そのためéは%C3%A9に、€は%E2%82%ACになります。違いは、それぞれがそのまま残す印字可能なASCII文字の集合だけです。

文字encodeURIencodeURIComponent
A-Z a-z 0-9 - _ . ! ~ * ' ( )そのままそのまま
# $ & + , / : ; = ? @そのままエンコード
空白と" % < > [ \ ] ^ ` { | }エンコードエンコード
制御文字と、ASCII以外のすべての文字UTF-8のバイトとしてエンコードUTF-8のバイトとしてエンコード

真ん中の行がすべてです。この11文字はURLの各部分を区切る役割を持つため、値の中でそのまま残すとURLの意味が変わってしまいます。

const s = 'Tom & Jerry/2?';
encodeURI(s);           // 'Tom%20&%20Jerry/2?'
encodeURIComponent(s);  // 'Tom%20%26%20Jerry%2F2%3F'

それぞれの使いどころ

URLに差し込むものには、すべてencodeURIComponentを使います。クエリパラメーターの値や名前、パスの1セグメント、フラグメント、そしてredirect=のようにパラメーターとして渡すURL全体です。もう一方の関数を使うと、次のような問題が起きます。

const q = 'rock & roll';

// Wrong: the & starts a new parameter
encodeURI('https://example.com/search?q=' + q);
// 'https://example.com/search?q=rock%20&%20roll'
// The server sees q = 'rock ' and a second parameter ' roll'

// Right
'https://example.com/search?q=' + encodeURIComponent(q);
// 'https://example.com/search?q=rock%20%26%20roll'

encodeURIが向いているのは、すでに完全で正しい構造のURLがあり、そこに空白やASCII以外の文字が含まれているだけという限られたケースです。たとえばhttps://example.com/café?name=ZoëのようなURLです。その場合でも、落とし穴が2つあります。

  • %をエンコードする。一部がすでにエンコードされているURLは二重エンコードされます。encodeURI('https://example.com/a%20b')はhttps://example.com/a%2520bを返します。
  • [と]をエンコードする。これらはIPv6アドレスを囲むのに使われるため、http://[::1]:8080/はhttp://%5B::1%5D:8080/となり、もはや有効なホストではなくなります。

部品からURLを組み立てるなら、たいていはnew URL()のほうが適しています。各部品を、それが現れる位置に応じてエンコードしてくれます。

const url = new URL('https://example.com/search');
url.searchParams.set('q', 'café & thé');
url.href;  // 'https://example.com/search?q=caf%C3%A9+%26+th%C3%A9'

RFC 3986との比較

RFC 3986は文字を2つのグループに分けています。非予約文字(第2.3節)はエンコードが一切不要な文字で、英字、数字、- . _ ~です。予約文字(第2.2節)は構文上の役割を持つ文字で、一般区切り文字: / ? # [ ] @と、サブ区切り文字! $ & ' ( ) * + , ; =があります。

JavaScriptの2つの関数は、どちらもこの区分と完全には一致しません。encodeURIComponentは、サブ区切り文字のうち! ' ( ) *の5つをエンコードせずに残します。ブラウザーやほとんどのサーバーは気にしませんが、エンコード済みのURLに対して署名を計算するシステムでは問題になります。OAuth 1.0の署名(RFC 5849)とAWS Signature Version 4はどちらも非予約文字以外すべてのエンコードを求めるため、アポストロフィや丸括弧を含む値で署名が一致しなくなることがあります。よく使われる対処法は次のとおりです。

function encodeRFC3986(str) {
  return encodeURIComponent(str).replace(/[!'()*]/g,
    c => '%' + c.charCodeAt(0).toString(16).toUpperCase());
}

encodeRFC3986("it's (fine)!*");  // 'it%27s%20%28fine%29%21%2A'

URLSearchParamsとプラス記号

URLSearchParams、したがってurl.searchParamsも、どちらの関数も使っていません。HTMLフォームが送信するapplication/x-www-form-urlencoded形式に従っており、違いは2つあります。空白は+になり、そのまま残るのは英字、数字、* - . _だけなので、! ' ( ) ~もエンコードされます。

encodeURIComponent('a b+c');                      // 'a%20b%2Bc'
new URLSearchParams({ q: 'a b+c' }).toString();   // 'q=a+b%2Bc'

どちらの結果もクエリ文字列としては正しいものです。問題が起きるのは、エンコード側とデコード側の解釈が食い違ったときです。decodeURIComponentは+を空白に戻さないので、decodeURIComponent('a+b%2Bc')はa+b+cを返しますが、new URLSearchParams('q=a+b%2Bc').get('q')はa b+cを返します。クエリ文字列はURLSearchParamsでデコードするか、decodeURIComponentを呼ぶ前に+を空白に置き換えてください。パスの中では、+はただのプラス記号です。

decodeURIとdecodeURIComponentの違い

デコード関数はエンコード関数と対になっています。decodeURIComponentはすべてのエスケープシーケンスをデコードします。decodeURIは、encodeURIならエンコードしなかったはずの文字# $ & + , / : ; = ? @のエスケープをそのまま残し、URLをデコードしても構造が変わらないようにします。decodeURI('%C3%A9%20%2F%3F')はé %2F%3Fを返し、decodeURIComponentはé /?を返します。

どちらも、100%のように%の後に16進数2桁が続かない場合や、éをLatin-1でエンコードするシステムから来た%E9のようにバイト列が正しいUTF-8でない場合に、URIError: URI malformed(ChromeとNode.jsが使うV8の文言)を投げます。エンコード関数も、絵文字の途中で切れた文字列のようにサロゲートペアの片割れだけがあると、同じエラーを投げます。

escape()は代わりになりません

escape()は古いコードのために残されているレガシー関数です。@ * _ + - . /をそのまま残し、UTF-8を使いません。U+00FFまでの文字はLatin-1の1バイトになり、それより上の文字は標準外の%uシーケンスになります。escape('café €')はcaf%E9%20%u20ACを返しますが、これはUTF-8を前提とするサーバーではデコードできず、decodeURIComponent('%E9')は例外を投げます。encodeURIComponentに置き換え、unescape()はdecodeURIComponentに置き換えてください。ただし、古い形式ですでに保存されたデータは変換が必要です。

二重エンコード

どのエンコード関数も%自体を%25にエンコードするため、2回実行すると%2520のようなシーケンスができます。encodeURIComponent(encodeURIComponent('a b'))はa%2520bを返します。生の値は、URLに入れるその瞬間に、ちょうど1回だけエンコードしてください。すでにエンコードされた状態で受け取った文字列を再びエンコードしてはいけません。正当な例外は、URLの中に別のURLを入れる場合だけです。https://example.com/?q=a%20bをパラメーターとしてエンコードすると正しく...q%3Da%2520bとなり、受け取る側が1回デコードすれば内側のURLが元に戻ります。

自分の入力で2つのモードを比べたいなら、URLエンコードツールにencodeURIComponentとencodeURIの切り替えがあります。URLデコードは入力と同時にデコードし、フォームデータと同じく+を空白として扱います。そのため、本物のプラス記号を含む値では%2Bとして渡す必要があります。

よくある質問

encodeURIComponentがエンコードしない文字はどれですか?

英字、数字、- _ . ! ~ * ' ( ) です。それ以外はすべて、/ ? & = # や空白も含めて、UTF-8のバイトとしてパーセントエンコードされます。

完全なURLにencodeURIを使ってもよいですか?

URLの構造がすでに正しく、空白やASCII以外の文字が含まれているだけの場合に限ります。部品からURLを組み立てるなら、各部品をencodeURIComponentでエンコードするか、URLとURLSearchParamsのAPIを使ってください。

URLSearchParamsが%20ではなく+を出力するのはなぜですか?

HTMLフォームのapplication/x-www-form-urlencoded形式を使っており、その形式では+が空白を表すからです。サーバーはクエリ文字列をその方式でデコードするので、クエリ文字列ではどちらの形も機能しますが、decodeURIComponentだけでは+は空白に戻りません。

escape()はencodeURIComponentと同じですか?

違います。escape()はレガシー関数で、ASCII以外の文字をUTF-8ではなくLatin-1や%uXXXXシーケンスとしてエンコードし、@ + / をエンコードせずに残します。代わりにencodeURIComponentを使ってください。