検索結果のページ番号、絞り込みに使ったタグ、キャンペーンの流入元——URL の ? より後ろに付くクエリ文字列には、いろいろな情報が入っています。これを自前で split("&") して分解するのは面倒なうえ、日本語のデコード漏れなどのバグも起きがちです。JavaScript には、この作業を任せられる URLSearchParams が用意されています。この記事では、生成のしかたから値の取得・追加・削除、ループ処理、エンコードの注意点までを、動かせるデモとあわせて解説します。
目次
URLSearchParams でできること
URLSearchParams は、?tag=css&page=2 のようなクエリ文字列を「キーと値の組の集まり」として扱うためのオブジェクトです。文字列を分解して読み取るだけでなく、値を書き換えたり足したりしてから、またクエリ文字列に戻すこともできます。%E5%85%A5%E9%96%80 のようなパーセントエンコードの解読も自動で行ってくれるので、日本語を含むパラメータも意識せずに扱えます。
ブラウザーだけでなく Node.js でも同じ名前で使えるため、フロントエンドとサーバーサイドで書き方を揃えられるのも利点です。
URLSearchParams を作る4つの方法
new URLSearchParams() の引数には、クエリ文字列のほか、オブジェクトや配列を渡せます。実際のページで今開いている URL のパラメータを読みたいときは、location.search(? を含むクエリ部分の文字列)を渡すのが基本形です。先頭の ? は自動で取り除かれるため、切り落とす処理は要りません。
// 1. 今開いているページのクエリ文字列から作る(一番よく使う形)
const params = new URLSearchParams(location.search);
// 2. 文字列から作る(先頭の ? はあってもなくてもよい)
const fromString = new URLSearchParams("?tag=css&page=2");
// 3. オブジェクトから作る
const fromObject = new URLSearchParams({ tag: "css", page: "2" });
// 4. [キー, 値] の配列から作る(同じキーを複数書ける)
const fromArray = new URLSearchParams([
["tag", "css"],
["tag", "js"],
]);
console.log(fromArray.toString()); // "tag=css&tag=js"
オブジェクトから作る方法は書きやすい反面、キーが重複できないため「同じキーに複数の値」を持たせられません。タグの複数選択のような用途では、配列で渡すか、あとで説明する append() を使います。
get・getAll・has で値を取り出す
値の取得に使うのは get() です。指定したキーの最初の値を文字列で返し、そのキーが無ければ null を返します。同じキーが複数あるとき、すべてまとめて受け取りたい場合は getAll() を使うと配列で返ります。キーの有無だけを調べたいなら has() が true / false を返します。
次のデモでは、上のテキスト欄に書いたクエリ文字列がその場で解析されます。キーや値を書き換えると、表と各メソッドの戻り値がどう変わるか確認できます。
<div class="demo">
<label class="demo__label" for="query">クエリ文字列(自由に書き換えられます)</label>
<input id="query" class="demo__input" type="text" value="?tag=css&tag=js&page=2&q=CSS 入門">
<table class="demo__table">
<thead>
<tr><th>キー</th><th>値</th></tr>
</thead>
<tbody id="rows"></tbody>
</table>
<pre class="demo__out" id="out"></pre>
</div>
body {
font-family: system-ui, sans-serif;
margin: 0;
padding: 16px;
color: #212529;
}
.demo__label {
display: block;
font-size: 13px;
font-weight: bold;
margin-bottom: 6px;
}
.demo__input {
width: 100%;
box-sizing: border-box;
padding: 8px 10px;
font-family: monospace;
font-size: 14px;
border: 1px solid #007bff;
border-radius: 6px;
}
.demo__table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 14px;
}
.demo__table th,
.demo__table td {
border: 1px solid #ced4da;
padding: 6px 10px;
text-align: left;
}
.demo__table th {
background: #e7f1ff;
color: #0a58ca;
}
.demo__table td {
font-family: monospace;
}
.demo__out {
background: #f8f9fa;
border-left: 4px solid #007bff;
border-radius: 4px;
padding: 12px;
margin: 0;
font-size: 13px;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-all;
}
const input = document.getElementById("query");
const rows = document.getElementById("rows");
const out = document.getElementById("out");
function render() {
// 文字列から作る。先頭の ? は自動で取り除かれる
const params = new URLSearchParams(input.value);
// for...of で [キー, 値] のペアを1組ずつ取り出す
rows.innerHTML = "";
for (const [key, value] of params) {
const tr = document.createElement("tr");
const tdKey = document.createElement("td");
const tdValue = document.createElement("td");
tdKey.textContent = key;
tdValue.textContent = value;
tr.append(tdKey, tdValue);
rows.appendChild(tr);
}
// 主なメソッドの戻り値を表示する
out.textContent = [
'get("tag") → ' + JSON.stringify(params.get("tag")),
'getAll("tag") → ' + JSON.stringify(params.getAll("tag")),
'has("page") → ' + params.has("page"),
'get("none") → ' + JSON.stringify(params.get("none")),
"toString() → " + params.toString(),
].join("\n");
}
input.addEventListener("input", render);
render();
値は必ず文字列として返る点に注意してください。?page=2 の 2 も文字列の "2" なので、計算に使うときは Number() や parseInt() で数値に変換します。
const params = new URLSearchParams("?page=2");
const page = Number(params.get("page")) || 1; // パラメータが無ければ 1 ページ目
console.log(page); // 2(数値)
よく使うメソッドとプロパティ
ここまでに出てきたものも含め、URLSearchParams の主なメンバーを整理しておきます。
| メソッド/プロパティ | 働き |
|---|---|
get(name) | 最初に見つかった値を返す。無ければ null |
getAll(name) | そのキーの値をすべて配列で返す |
has(name) | そのキーがあるかを true / false で返す |
set(name, value) | 既存の値を上書きする。無ければ末尾に追加する |
append(name, value) | 既存の値を残したまま末尾に追加する |
delete(name) | そのキーの値をすべて削除する |
sort() | キー名の順に並べ替える(自身を並べ替える) |
toString() | クエリ文字列に戻す(先頭に ? は付かない) |
size | パラメータの個数(比較的新しいブラウザーで利用可能) |
set と append の違い
値を足すメソッドが2つあるのは、「同じキーを1つに保ちたいのか、複数持たせたいのか」で使い分けるためです。set() は既にそのキーがあればすべて置き換えて1つにまとめ、無ければ末尾に追加します。append() は既存の値を消さずに、同じキーをもう1組足します。
// set:同じキーが複数あっても1つにまとめられる
const a = new URLSearchParams("a=1&b=2&a=3");
a.set("a", "9");
console.log(a.toString()); // "a=9&b=2"
// append:既存の値を残して追加される
const b = new URLSearchParams("a=1&b=2&a=3");
b.append("a", "9");
console.log(b.toString()); // "a=1&b=2&a=3&a=9"
// delete:そのキーの値をまとめて削除する
b.delete("a");
console.log(b.toString()); // "b=2"
ページ番号や並び順のように「1つだけ持てばよい」値は set()、チェックボックスで選んだタグのように「複数選べる」値は append()、と考えると迷いません。delete() はそのキーの値をすべて消すので、絞り込みを解除する処理にそのまま使えます。
すべてのパラメータをループで取り出す
URLSearchParams は反復可能(イテラブル)なので、for...of でそのまま回せます。1回のループで [キー, 値] の配列が取れるため、分割代入で受け取ると読みやすくなります。キーだけ・値だけが欲しいときは keys() と values()、明示的にペアを取りたいときは entries() を使います(for...of に直接渡した場合と entries() は同じ結果です)。
const params = new URLSearchParams("?tag=css&tag=js&page=2");
// キーと値のペアを順に取り出す
for (const [key, value] of params) {
console.log(key, value);
}
// tag css / tag js / page 2
// キーだけ・値だけを取り出す
console.log([...params.keys()]); // ["tag", "tag", "page"]
console.log([...params.values()]); // ["css", "js", "2"]
// forEach も使える(引数の順は 値, キー)
params.forEach((value, key) => {
console.log(`${key} = ${value}`);
});
同じキーが複数あると、その回数だけループが回ります。keys() の結果に tag が2回現れているのはそのためです。全パラメータをまとめて普通のオブジェクトにしたいときは Object.fromEntries(params) が便利ですが、重複したキーは最後の値だけが残る点に注意してください。
URL オブジェクトと組み合わせる
URL 全体を扱うときは URL オブジェクトが便利です。URL には searchParams プロパティがあり、その中身はまさに URLSearchParams です。しかもこれは元の URL とつながっているため、url.searchParams.set(...) で値を変えると url.href や url.search にもすぐ反映されます。文字列を自分で連結する必要がなく、? と & の付け忘れも起きません。
const url = new URL("https://example.com/search?q=old");
url.searchParams.set("q", "新しい 値"); // 既存の q を上書き
url.searchParams.append("tag", "css"); // タグを追加
console.log(url.toString());
// "https://example.com/search?q=%E6%96%B0%E3%81%97%E3%81%84+%E5%80%A4&tag=css"
次のデモは、この仕組みでフォームの入力値から検索 URL を組み立てる例です。キーワードや並び順を変えたり、タグのチェックを増やしたりすると、下に表示される URL がその場で変わります。キーワードを空にすると q が付かなくなる点にも注目してください。
<form id="search-form" class="form">
<div class="form__row">
<label class="form__label" for="keyword">キーワード</label>
<input id="keyword" class="form__input" name="keyword" type="text" value="CSS 入門">
</div>
<div class="form__row">
<label class="form__label" for="sort">並び順</label>
<select id="sort" class="form__input" name="sort">
<option value="new">新着順</option>
<option value="popular">人気順</option>
</select>
</div>
<div class="form__row">
<span class="form__label">タグ(複数選択可)</span>
<label class="form__check"><input type="checkbox" name="tag" value="css" checked> css</label>
<label class="form__check"><input type="checkbox" name="tag" value="js"> js</label>
<label class="form__check"><input type="checkbox" name="tag" value="wordpress"> wordpress</label>
</div>
</form>
<p class="result__title">組み立てられた URL</p>
<pre class="result" id="url-output"></pre>
body {
font-family: system-ui, sans-serif;
margin: 0;
padding: 16px;
color: #212529;
}
.form {
border: 1px solid #ced4da;
border-radius: 8px;
padding: 14px 16px;
}
.form__row {
margin-bottom: 14px;
}
.form__row:last-child {
margin-bottom: 0;
}
.form__label {
display: block;
font-size: 13px;
font-weight: bold;
margin-bottom: 6px;
}
.form__input {
width: 100%;
box-sizing: border-box;
padding: 7px 10px;
font-size: 14px;
border: 1px solid #ced4da;
border-radius: 6px;
}
.form__check {
display: inline-block;
margin-right: 14px;
font-size: 14px;
}
.result__title {
font-size: 13px;
font-weight: bold;
margin: 18px 0 6px;
}
.result {
background: #f8f9fa;
border-left: 4px solid #007bff;
border-radius: 4px;
padding: 12px;
margin: 0;
font-size: 13px;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-all;
}
const form = document.getElementById("search-form");
const output = document.getElementById("url-output");
function buildUrl() {
const url = new URL("https://example.com/search");
const keyword = form.elements.keyword.value.trim();
if (keyword !== "") {
// set は同じキーがあれば上書きする
url.searchParams.set("q", keyword);
}
url.searchParams.set("sort", form.elements.sort.value);
// 同じキーで複数の値を持たせたいときは append
const checked = form.querySelectorAll('input[name="tag"]:checked');
for (const box of checked) {
url.searchParams.append("tag", box.value);
}
output.textContent = url.toString();
}
form.addEventListener("input", buildUrl);
buildUrl();
フォームの入力欄が多いときは、FormData を経由するともっと短く書けます。FormData は [名前, 値] のペアを順に取り出せるので、そのまま URLSearchParams の引数に渡せます。チェックボックスのように同じ名前が複数あっても、それぞれのペアがそのまま引き継がれます(ファイル選択欄がある場合はこの方法では扱えません)。
const form = document.querySelector("#search-form");
form.addEventListener("submit", (event) => {
event.preventDefault();
// フォームの name 属性と入力値がそのままパラメータになる
const params = new URLSearchParams(new FormData(form));
location.href = "/search?" + params.toString();
});
ページを移動せずに URL を書き換える
絞り込みの結果を JavaScript で書き換えるページでは、その状態を URL にも残しておきたくなります。URL に残しておけば、そのままブックマークしたり誰かに送ったりでき、再読み込みしても同じ絞り込みで表示できます。ページを読み込み直さずに URL だけを変えるには、history.replaceState() を使います。
replaceState() は現在の履歴エントリーを置き換えるので、ブラウザーの「戻る」の履歴は増えません。絞り込みを操作するたびに履歴が増えるのを避けたいときに向いています。逆に、絞り込みごとに「戻る」で1つ前の状態に戻せるようにしたいなら history.pushState() を使います。
// 現在の URL をもとに URLSearchParams を用意する
function updateQuery(key, value) {
const params = new URLSearchParams(location.search);
if (value === "") {
params.delete(key); // 値が空なら絞り込みを外す
} else {
params.set(key, value);
}
// パラメータが1つも無ければ ? を付けない
const query = params.toString();
const newUrl = query === "" ? location.pathname : location.pathname + "?" + query;
// 第1引数は履歴に保存する状態、第2引数は使われない(空文字でよい)
history.replaceState(null, "", newUrl);
}
document.querySelector("#category").addEventListener("change", (event) => {
updateQuery("category", event.target.value);
});
ページを開いたときは逆に location.search からパラメータを読み、その値でフォームや一覧の初期状態を復元します。この「読み込み時に URL から復元し、操作時に URL へ書き戻す」という往復が、絞り込み状態を URL で保持する仕組みの基本形です。
日本語や記号は自動でエンコードされる
toString() で書き出すとき、値はフォーム送信と同じ形式(application/x-www-form-urlencoded)に変換されます。日本語は %E5%85%A5%E9%96%80 のようなパーセントエンコードになり、半角スペースは + に置き換わります。%20 ではなく + になるのがこの形式の特徴です。
const params = new URLSearchParams();
params.set("q", "CSS 入門");
console.log(params.toString()); // "q=CSS+%E5%85%A5%E9%96%80"
// 読み取るときは自動でデコードされる(+ はスペースに戻る)
console.log(new URLSearchParams("q=CSS+%E5%85%A5%E9%96%80").get("q")); // "CSS 入門"
裏を返すと、解析するときの + はスペースとして読まれます。?q=1+1 を解析すると値は "1 1" になり、プラス記号は残りません。プラス記号そのものを値に含めたいときは %2B と書く必要がありますが、set() や append() で値を渡す場合は URLSearchParams 側が %2B に変換してくれるので、自分で気をつける必要はありません。
エンコードが自動なので、encodeURIComponent() を通した文字列を set() に渡すのは避けてください。% がさらにエンコードされて %25 になり、二重エンコードされた値ができてしまいます。渡すのは生の文字列だけで十分です。
値がうまく取れないときに確認すること
get() が null になる、値が思ったものと違う、というときは、たいてい次のどれかに当てはまります。
URL 全体を渡してしまっている
一番多いのが new URLSearchParams(location.href) のように、URL 全体を渡してしまうケースです。コンストラクターが取り除いてくれるのは先頭の ? だけで、それより前の部分は最初のキー名の一部として扱われます。https://example.com/?a=1 を渡すと、キーは a ではなく https://example.com/?a になってしまい、get("a") は null を返します。
渡すのは location.search(クエリ部分だけ)にするか、URL 文字列を扱うなら new URL(urlString).searchParams のように URL オブジェクトを経由します。URL を経由すれば、末尾のハッシュ(#section)も自動で切り分けられます。
const href = "https://example.com/list?a=1#section";
// NG: URL 全体を渡すとキー名が壊れる
console.log(new URLSearchParams(href).get("a")); // null
// OK: URL オブジェクト経由なら正しく取れる
console.log(new URL(href).searchParams.get("a")); // "1"
キー名の表記が一致していない
キー名は大文字と小文字を区別します。URL 側が ?Page=2 なら get("page") では取れません。前後に余計なスペースが入っている場合も別のキーとして扱われるので、うまく取れないときは [...params.keys()] でキーの一覧を出して、実際にどんな名前で入っているかを確かめると早く原因が分かります。
値が空文字のパラメータを条件に使っている
?q= のように値が空のときは、get("q") は null ではなく空文字 "" を返します。空文字は条件式では偽と判定されるため、if (params.get("q")) と書くと「パラメータが無い」ときと同じ扱いになってしまいます。キーの有無そのものを判定したいなら has() を使い、値が入っているかどうかを見たいなら get() の結果を調べる、と使い分けてください。
まとめ
URLSearchParams を使えば、クエリ文字列の解析も組み立ても数行で済みます。読み取りは get()・getAll()・has()、書き込みは上書きの set() と追加の append()、削除は delete() が基本です。URL 全体を扱うときは URL オブジェクトの searchParams を使うと、区切り記号やエンコードを気にせず組み立てられます。値が取れないときは、location.search ではなく URL 全体を渡していないか、キー名の表記が合っているかをまず確認してみてください。