JavaScriptを学び始めたばかりの頃、こんなことで悩んでいませんか?
- 配列(Array)を宣言したけど、実際の使いどころが分からない
- Math.random() を使ったが、仕組みを説明できない
- ランダム表示なのに、同じ結果ばかり出て不安になる
- LocalStorageって何? いつ使えばいいの?
この記事では、HTML+CSS+JavaScriptだけで動く
「ランダム名言メーカー」を作りながら、
- JavaScriptの配列の使い方
- Math.randomで配列からランダムに値を取り出す方法
- 状態(current)を使った実装の考え方
- LocalStorageでお気に入りデータを保存する方法
を、初心者でも理解・説明できるレベルまで丁寧に解説します。
この記事で解決できる悩み
この記事を読むことで、次のような悩みが解消されます。
- 配列を「データの置き場」として使えるようになる
- Math.randomの意味と役割が分かる
- 「ランダムなのに同じ結果が続く」問題を防ぐ考え方が分かる
- LocalStorageでデータを保存する理由が理解できる
- ボタン操作・キーボード操作を整理して実装できる
なぜ「ランダム名言メーカー」が初心者学習に向いているのか
名言メーカーは、JavaScript初心者にとって非常に相性の良い教材です。
- 名言データが複数ある → 配列を使う理由が明確
- 1つを選んで表示する → Math.randomの用途が自然
- 今表示中の名言を扱う → 状態管理(current)の入門
- お気に入り保存 → LocalStorageの基礎を学べる
「作って終わり」ではなく、
なぜこの書き方になるのかを説明しやすい構成になっています。
完成イメージと今回のゴール
今回作成するアプリの仕様は以下の通りです。
- 名言データを 配列(QUOTES) で管理
- Math.randomを使った ランダム表示
- 同じ名言が連続しにくい工夫あり
- ボタン操作
- 新しい名言
- コピー
- お気に入り(LocalStorage保存)
- X共有
- キーボード操作
- Space:新しい名言
- c:コピー
- f:お気に入り
- アクセシビリティ対応(aria-live / aria-pressed)
- ページを開いたら 必ず最初に名言が1つ表示される
このアプリで使う「状態」と「データ」を整理する
変数と役割一覧
| 変数名 | 種類 | 役割 |
|---|---|---|
| QUOTES | 配列 | 名言データの一覧 |
| current | オブジェクト | 今表示している名言 |
| favorites | 配列 | お気に入り名言のID一覧 |
重要ポイント
- QUOTES:固定データ
- current:現在の状態
- favorites:保存する状態(LocalStorage)
JavaScriptの配列で名言データを管理する理由
名言を1つずつ変数にするのではなく、配列でまとめて管理します。
理由
- データの追加・削除が簡単
- ランダム取得がしやすい
- IDを使って検索できる
お気に入り保存では「名言全文」ではなく、
IDだけをLocalStorageに保存することで管理が楽になります。
Math.randomで配列からランダムに値を取り出す方法
ランダム表示の基本形は以下です。
Math.floor(Math.random() * 配列.length)
考え方
- Math.random() → 0以上1未満の小数
- 配列の長さを掛ける → 配列の範囲に収まる
- Math.floorで整数にする → indexとして使える
同じ名言が続かないようにする工夫
ランダム処理では、同じ結果が連続することがあります。
そこで今回は、
- 直前の名言(current)と
- 新しく引いた名言
の IDが同じなら引き直す 仕組みを入れています。
無限ループを防ぐため、引き直し回数には上限を設けています。
current変数が果たす役割(状態管理の基礎)
currentは「今表示されている名言」を一元管理する変数です。
これにより、
- コピー
- お気に入り登録
- X共有
すべてが 同じcurrentを参照 でき、コードが整理されます。
LocalStorageでお気に入りデータを保存する方法
保存構造
- キー:
fav-quotes - 値:
["q1","q4","q6"](JSON文字列)
なぜID配列なのか
- データ本体はQUOTESにある
- 保存容量が小さい
- 表示時にfindで簡単に復元できる
アクセシビリティ対応について
- aria-live:名言が切り替わったことを伝える
- aria-pressed:お気に入りのON / OFF状態を明示
初心者向け教材でも、こうした配慮を入れることは大切です。
キーボード操作を実装する際の注意点
- Spaceキーはデフォルト動作を止める(preventDefault)
- 入力欄がある場合はキー競合に注意
よくある失敗と対処法(FAQ形式)
Q1. Math.random()って何?
→ 0以上1未満のランダムな小数を返す関数です。
Q2. 同じ名言ばかり出るのはバグ?
→ バグではありません。ランダムの仕様です。直前と同じIDを避けることで改善できます。
Q3. お気に入りが保存されない
→ JSON.stringify / JSON.parse を忘れていないか確認してください。
Q4. 初期表示で名言が出ない
→ 初期化時に randomQuote() を呼びましょう。
Q5. コピーが動かない
→ HTTPS環境で試してください。
Q6. Spaceキーで画面が動く
→ preventDefault() を忘れずに。
発展アイデア(次の学習につながる)
- 名言ジャンル切り替え
- 履歴機能の追加
- お気に入り並び替え
- 入力フォームで名言追加
学びのまとめ
- 配列は「複数データを管理するための基本構造」
- Math.randomは「配列のindexを作る」と理解すると使いやすい
- 状態(current)を持つことで処理が整理される
- LocalStorageはID配列保存が扱いやすい
前後記事への導線
- 前の記事:Day02「状態管理の基礎」
- 次の記事:Day04「入力処理とLocalStorage」
Day03 完成コード
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Day03 — ランダム名言メーカー</title>
<meta name="description" content="ボタン1つで名言をランダム表示。コピー・お気に入り保存・X共有対応のシンプルなWebアプリ。">
<style>
:root{
--bg:#0b1020; --panel:#121836; --text:#e9ecf1; --muted:#a7b0c3; --accent:#7aa2ff;
--ok:#6efacc; --warn:#ffb347; --shadow:0 10px 24px rgba(0,0,0,.35)
}
*{box-sizing:border-box}
body{
margin:0;min-height:100vh;display:flex;justify-content:center;align-items:center;
background:radial-gradient(1000px 500px at 80% -10%,#16204a,transparent),var(--bg);
font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Noto Sans JP",sans-serif;
color:var(--text)
}
.app{
width:min(96vw,640px);background:var(--panel);border:1px solid rgba(255,255,255,.08);
border-radius:18px;box-shadow:var(--shadow);padding:24px
}
h1{margin:0 0 8px;font-size:20px}
.muted{color:var(--muted);font-size:12px}
.quote{
margin:20px 0;padding:18px;border-radius:14px;background:#0f1430;box-shadow:inset 0 0 0 1px #2a3566;
min-height:100px;display:flex;flex-direction:column;justify-content:center;gap:12px
}
.quote-text{font-size:clamp(18px,5vw,28px);line-height:1.5}
.quote-author{font-size:16px;text-align:right;color:var(--muted)}
.quote-source{font-size:12px;text-align:right;color:var(--muted)}
.buttons{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px}
button{
border:0;border-radius:12px;padding:10px 14px;font-weight:600;cursor:pointer;
background:#0d1330;color:var(--text);border:1px solid #2a3566;transition:.2s
}
button:hover{background:#182050}
.btn.primary{background:linear-gradient(180deg,var(--accent),#557aff);color:#071022}
.btn.fav[aria-pressed="true"]{background:var(--ok);color:#071022}
.favorites{margin-top:20px}
.favorites h2{font-size:14px;color:var(--muted);margin-bottom:6px}
.fav-list{display:flex;flex-wrap:wrap;gap:6px}
.fav-item{
padding:4px 8px;border-radius:8px;background:#0f1430;font-size:12px;cursor:pointer;
border:1px solid #2a3566;color:var(--muted)
}
.sr{position:absolute;left:-9999px}
</style>
</head>
<body>
<main class="app" role="application" aria-labelledby="title">
<h1 id="title">Day03 — ランダム名言メーカー</h1>
<div class="muted">ボタン1つで名言を表示。コピー・お気に入り・X共有対応。</div>
<section class="quote" aria-live="polite" aria-atomic="true">
<div id="quote-text" class="quote-text">名言がここに表示されます</div>
<div id="quote-author" class="quote-author"></div>
<div id="quote-source" class="quote-source"></div>
</section>
<div class="buttons">
<button id="new" class="btn primary">新しい名言</button>
<button id="copy" class="btn">コピー</button>
<button id="fav" class="btn fav" aria-pressed="false">お気に入り</button>
<button id="share" class="btn">X共有</button>
</div>
<section class="favorites">
<h2>お気に入り</h2>
<div id="fav-list" class="fav-list"></div>
</section>
<div id="a11y" class="sr" aria-live="assertive"></div>
</main>
<script>
// 簡易データ
const QUOTES = [
{id:"q1", text:"学びて時にこれを習う。また説ばしからずや。", author:"孔子", source:"論語"},
{id:"q2", text:"天才は1%のひらめきと99%の努力。", author:"トーマス・エジソン"},
{id:"q3", text:"成功は最良の教師ではない。失敗こそが人を謙虚にする。", author:"ビル・ゲイツ"},
{id:"q4", text:"Stay hungry, stay foolish.", author:"スティーブ・ジョブズ"},
{id:"q5", text:"千里の道も一歩から。", author:"老子"},
{id:"q6", text:"昨日から学び、今日を生き、明日へ希望を。", author:"アルベルト・アインシュタイン"}
];
const $ = (s)=>document.querySelector(s);
const textEl = $('#quote-text');
const authorEl = $('#quote-author');
const sourceEl = $('#quote-source');
const favBtn = $('#fav');
const favList = $('#fav-list');
const a11y = $('#a11y');
let current = null;
let favorites = JSON.parse(localStorage.getItem('fav-quotes')||'[]');
function randomQuote(){
let q;
for(let i=0;i<5;i++){
q = QUOTES[Math.floor(Math.random()*QUOTES.length)];
if(!current || q.id !== current.id) break;
}
showQuote(q);
}
function showQuote(q){
current = q;
textEl.textContent = q.text;
authorEl.textContent = q.author? `— ${q.author}` : "";
sourceEl.textContent = q.source||"";
favBtn.setAttribute('aria-pressed', favorites.includes(q.id));
}
function copyQuote(){
if(!current) return;
const str = `「${current.text}」 — ${current.author||""}${current.source? "("+current.source+")":""}`;
navigator.clipboard.writeText(str);
a11y.textContent="コピーしました";
}
function toggleFav(){
if(!current) return;
const i = favorites.indexOf(current.id);
if(i>=0) favorites.splice(i,1);
else favorites.push(current.id);
localStorage.setItem('fav-quotes', JSON.stringify(favorites));
favBtn.setAttribute('aria-pressed', favorites.includes(current.id));
renderFavs();
}
function renderFavs(){
favList.innerHTML="";
favorites.slice(-20).forEach(id=>{
const q = QUOTES.find(x=>x.id===id);
if(!q) return;
const div=document.createElement('div');
div.className='fav-item';
div.textContent=q.text.slice(0,12)+"…";
div.title=q.text;
div.onclick=()=>showQuote(q);
favList.appendChild(div);
});
}
function shareX(){
if(!current) return;
const str = `「${current.text}」 — ${current.author||""}`;
const url = location.href;
const share = `https://twitter.com/intent/tweet?text=${encodeURIComponent(str)}&url=${encodeURIComponent(url)}&hashtags=Quote,100DaysOfCode`;
window.open(share,'_blank');
}
// イベント
$('#new').onclick=randomQuote;
$('#copy').onclick=copyQuote;
favBtn.onclick=toggleFav;
$('#share').onclick=shareX;
window.addEventListener('keydown',e=>{
if(e.code==='Space'){e.preventDefault();randomQuote();}
if(e.key==='c') copyQuote();
if(e.key==='f') toggleFav();
});
// 初期化
renderFavs();
randomQuote();
</script>
</body>
</html>
