Q1一度も保存していないキーで localStorage.getItem("theme") を実行すると?
localStorage — ブラウザに保存する
ブラウザに値を保存する localStorage の setItem・getItem・removeItem、値が文字列に変わるため JSON で配列を保存する書き方、無いキーで返る null、容量の上限を扱います。
カートに入れた商品や、選んだ表示の設定を変数に入れておいても、再読み込みやページを閉じたときに消え、次に開くと最初の状態に戻ります。ここまでのコードには、ページを開き直しても値を残せる場所がありません。
この記事では、ブラウザに値を保存する localStorage と、配列やオブジェクトを保存するための JSON の文字列を扱います。
ページを閉じても値を残す — setItem と getItem
ニュースサイトで、利用者が選んだ文字サイズを、次に開いたときも使いたいとします。const fontSize = "large"; のように変数へ入れても、変数は再読み込みやページを閉じたときに消えるので、次に開いたページからは読めません。
localStorage(ブラウザが持つ、キーと値の保存場所)は、setItem(キー, 値) で保存し、getItem(キー) で読み出し、removeItem(キー) で取り除きます。
保存場所はオリジン(https://news.example/settings のパスより前の部分)ごとに分かれ、同じオリジンのページは同じ値を読み書きします。
// 以前に保存した値が残っていれば取り除き、何も保存していない状態から始める
localStorage.removeItem("fontSize");
// 利用者が文字サイズを「大」に切り替えたときに保存する
localStorage.setItem("fontSize", "large");
// ページを開いたときに、保存した値を読み出して使う
const fontSize = localStorage.getItem("fontSize");
console.log(fontSize); // large
// 同じキーで保存すると、前の値を置き換える
localStorage.setItem("fontSize", "small");
console.log(localStorage.getItem("fontSize")); // small
- このオリジンのページとタブが、1 つの localStorage を共有する
"fontSize"→"small"(同じキーへの保存で置き換わった値)- ページを閉じても、再読み込みしても残る
const fontSizeなどの変数- 再読み込みすると、変数は作り直される
- 別の localStorage を持つ
- news.example が保存した
"fontSize"は読めない
ページを開いたときに getItem で読み、利用者が設定を変えたときに setItem で書けば、再読み込みの後も同じ文字サイズで表示できます。保存した値は、ページのコードで取り除くか、利用者がブラウザの設定から消すまで残ります。
練習問題の保存は次の実行にも残る
練習問題のコンソールは、サイト内のすべての JavaScript の演習を同じオリジンで実行するので、localStorage も 1 つを共有し、ページを開き直しても残ります。前の実行で保存した値が結果に混ざらないよう、演習ではコードの最初で、使うキーを removeItem で取り除いてから始めます。
配列やオブジェクトを保存する — JSON.stringify
旅行予約サイトで、お気に入りに入れたホテルを配列で持ち、次に開いたときも一覧に出したいとします。配列を setItem にそのまま渡してもエラーにはなりませんが、読み出した値からは、ホテルの番号も名前も取り出せません。
setItem は、渡された値を文字列に変換してから保存します。配列やオブジェクトは JSON.stringify で JSON の文字列にしてから保存し、読み出した文字列を JSON.parse で元の配列やオブジェクトに戻します。
// 以前に保存した値が残っていれば取り除いてから始める
localStorage.removeItem("favoriteHotels");
const favoriteHotels = [{ id: "H-101", name: "港ホテル" }, { id: "H-204", name: "駅前イン" }];
// そのまま渡すと、保存の時点で文字列に変換される
localStorage.setItem("favoriteHotels", favoriteHotels);
console.log(localStorage.getItem("favoriteHotels")); // [object Object],[object Object]
// JSON の文字列にしてから保存する
localStorage.setItem("favoriteHotels", JSON.stringify(favoriteHotels));
const saved = localStorage.getItem("favoriteHotels");
console.log(saved); // [{"id":"H-101","name":"港ホテル"},{"id":"H-204","name":"駅前イン"}]
// 読み出した文字列を、配列に戻してから使う
const restored = JSON.parse(saved);
console.log(restored[1].name); // 駅前イン
エラーにならないため、数値や真偽値でも、読み出した後の計算や if の判定で初めて食い違いに気づきます。下の表は、setItem に渡した値ごとに、保存される文字列と、読み出したときに起きることをまとめたものです。
| setItem に渡す値 | 保存される文字列 | 読み出したときに起きること |
|---|---|---|
| 数値の 20 | "20" | "20" + 1 が "201" になる(Number で数に直す) |
| 真偽値の false | "false" | 空でない文字列なので if の条件では true になる(JSON.parse で false に戻す) |
| 文字列の配列 ["A-1", "B-2"] | "A-1,B-2" | カンマでつながった 1 つの文字列になり、配列ではなくなる |
保存が無いキーを読む — getItem が返す null
レシピサイトで、見たレシピの番号を閲覧履歴として保存し、ページを開いたときに件数を表示したいとします。初めて開いた人や履歴を消した人には保存が無いので、保存がある前提で JSON.parse の結果の length を読むと、その行で TypeError になって止まります。
getItem は、保存されていないキーを渡すと undefined ではなく null を返します。JSON.parse(localStorage.getItem(キー)) ?? [] と書けば、保存が無いときだけ空の配列が使われます。
// 閲覧履歴を読み出す。保存が無ければ空の配列にする
function loadHistory() {
return JSON.parse(localStorage.getItem("viewHistory")) ?? [];
}
// 初めて開いたとき — 保存が無いので、getItem は null を返す
localStorage.removeItem("viewHistory");
console.log(localStorage.getItem("viewHistory")); // null
console.log(loadHistory().length); // 0
// 保存してあるとき — 文字列から配列に戻る
localStorage.setItem("viewHistory", JSON.stringify(["R-12", "R-30"]));
console.log(loadHistory().length); // 2
// removeItem で取り除いた後 — また null になる
localStorage.removeItem("viewHistory");
console.log(loadHistory().length); // 0
読み出しを関数にまとめておけば、ログアウトで removeItem を呼んだ後も、呼び出し側は null を確かめずに配列として扱えます。removeItem は保存の無いキーに呼んでもエラーにならないので、取り除く前に保存があるかを調べる必要もありません。
再読み込みしてもカートを戻す — 保存と復元の関数
ネットショップで、カートに商品を入れた後にページを再読み込みしても、中身をそのまま表示したいとします。cart.push で商品を加えても変わるのは変数だけで、保存の行を書き忘れた場所があると、再読み込みでその商品が消えます。
保存を saveCart、読み込みを loadCart の 2 つの関数にまとめ、カートを変えた直後とページを開いたときに呼びます。setItem は同期の処理(保存を終えてから次の行へ進む処理)なので、fetch と違って await を付けなくても、次の行で保存した値を読めます。
localStorage.removeItem("cart"); // 以前に保存したカートが残っていれば取り除く
// カートを JSON の文字列にして保存する。await は要らない
function saveCart(cart) {
localStorage.setItem("cart", JSON.stringify(cart));
}
// 保存したカートを配列に戻す。保存が無ければ空のカート
function loadCart() {
return JSON.parse(localStorage.getItem("cart")) ?? [];
}
// ページを開いたときに読み込み、商品を入れたら保存する
const cart = loadCart();
cart.push({ id: "K-310", qty: 1 });
saveCart(cart);
// 再読み込みの代わりに、変数 cart を使わず保存した文字列から読み込み直す
const reloaded = loadCart();
console.log(reloaded.length, reloaded[0].id); // 1 K-310
カートを変える処理を 1 つの関数にまとめ、その最後で saveCart を呼ぶ形にしておけば、呼び出す場所ごとに保存を書く必要がなくなり、呼び忘れも起きません。読み込みも loadCart にまとめてあるので、?? [] を書き忘れる場所もありません。
容量を超えると setItem が例外を投げる
Chrome では、1 つのオリジンに保存できるのはキーと値を合わせて約 524 万文字で、超えると setItem が QuotaExceededError を投げ、値は保存されません。読み書きは同期なので、大きな値を保存している間はページの操作も止まります。カートや設定のような小さな値に使います。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2localStorage.setItem("darkMode", false) の後、読み出した値を if の条件にすると?
Q3cart.push(book) の後に saveCart を呼ばず再読み込みすると、loadCart() で戻るのは?