Day03|配列とMath.randomが理解できるランダム名言メーカー【JavaScript初心者向け】

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)

考え方

  1. Math.random() → 0以上1未満の小数
  2. 配列の長さを掛ける → 配列の範囲に収まる
  3. 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配列保存が扱いやすい

前後記事への導線


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>
タイトルとURLをコピーしました