JavaScript | JavaScript 演算子の優先順位 一覧表(初心者向け)

javascrpit JavaScript
スポンサーリンク

ブラウザ上で自動採点つき練習アプリ(HTML + JS)

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>JS 演算子 練習アプリ(自動採点)</title>
  <style>
    :root{--bg:#0f1724;--card:#0b1220;--accent:#7dd3fc;--muted:#94a3b8}
    body{font-family:Inter,ui-sans-serif,system-ui,Segoe UI,Roboto,"Helvetica Neue",Arial; background:linear-gradient(180deg,#071021, #05203a); color:#e6eef6; margin:0; padding:24px}
    .container{max-width:980px;margin:0 auto}
    header{display:flex;gap:16px;align-items:center}
    h1{margin:0;font-size:20px}
    p.lead{color:var(--muted);margin:6px 0 20px}
    .card{background:var(--card);border-radius:12px;padding:16px;margin-bottom:12px;box-shadow:0 6px 18px rgba(3,7,18,.6)}
    pre.code{background:#071226;padding:12px;border-radius:8px;overflow:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-size:13px}
    label{display:block;margin:8px 0 6px;color:var(--muted);font-size:13px}
    input[type=text], textarea{width:100%;padding:10px;border-radius:8px;border:1px solid #15324a;background:#061827;color:#e6eef6}
    textarea{min-height:56px}
    .row{display:flex;gap:8px}
    button{background:transparent;border:1px solid rgba(125,211,252,.18);color:var(--accent);padding:8px 12px;border-radius:10px;cursor:pointer}
    button.primary{background:linear-gradient(90deg,#0369a1,#0891b2);border:0;color:white}
    .hint{color:#9bd0e8;margin-top:8px}
    .explain{background:#05283b;padding:10px;border-radius:8px;margin-top:8px;color:var(--muted)}
    .ok{color:#86efac}
    .ng{color:#fda4af}
    .score{font-weight:700;color:var(--accent)}
    footer{color:var(--muted);margin-top:18px;font-size:13px}
  </style>
</head>
<body>
  <div class="container">
    <header>
      <div>
        <h1>JavaScript 演算子 練習アプリ(自動採点)</h1>
        <p class="lead">コードを読んで予想される出力を入力し、「採点」を押すと正誤・解説・合計スコアが表示されます。ブラウザの開発者コンソールではなくこのページ内で評価します。</p>
      </div>
    </header>

    <div id="exercises"></div>

    <div class="card">
      <div style="display:flex;justify-content:space-between;align-items:center">
        <div>合計スコア: <span id="score" class="score">0</span> / <span id="total">0</span></div>
        <div class="row">
          <button id="checkAll" class="primary">すべて採点</button>
          <button id="reset">リセット</button>
        </div>
      </div>
    </div>

    <footer>この練習は学習用です。コードはページ内で安全に評価しています(与えられたスニペットのみを実行)。複雑な式は括弧で順序を明示する癖をつけましょう。</footer>
  </div>

<script>
const exercises = [
  {
    id: 1,
    title: '問題①:数値の計算',
    code: `let result = 2 + 3 * 4; console.log(result);`,
    hint: '掛け算は足し算より優先される',
  },
  {
    id: 2,
    title: '問題②:複合代入',
    code: `let x = 5; x += 3; x *= 2; console.log(x);`,
    hint: '左から順に実行される(逐次的)',
  },
  {
    id: 3,
    title: '問題③:型変換を含む比較',
    code: `console.log(3 == "3"); console.log(3 === "3");`,
    hint: '== は型変換を行うが === は型も一致させる',
  },
  {
    id: 4,
    title: '問題④:短絡評価(ショートサーキット)',
    code: `console.log(0 || "default"); console.log("hello" && 123); console.log(null ?? "fallback");`,
    hint: '|| は左が falsy のとき右を返す、&& は左が truthy のとき右を返す。?? は nullish(null/undefined)のときだけ右を使う',
  },
  {
    id: 5,
    title: '問題⑤:条件(三項)演算子',
    code: `let age = 20; let result = (age >= 18) ? "大人" : "未成年"; console.log(result);`,
    hint: '条件 ? 真の値 : 偽の値',
  },
  {
    id: 6,
    title: '問題⑥:論理代入(モダン構文)',
    code: `let a = null; let b = 0; a ??= "default"; b ||= 10; console.log(a, b);`,
    hint: '??= は nullish のとき代入。||= は falsy のとき代入。',
  },
  {
    id: 7,
    title: '問題⑦:右結合を理解する',
    code: `let a = 2 ** 3 ** 2; console.log(a);`,
    hint: '**(べき乗)は右結合',
  },
  {
    id: 8,
    title: '問題⑧:代入も右結合',
    code: `let x, y, z; x = y = z = 5; console.log(x, y, z);`,
    hint: '代入は右から評価される',
  },
  {
    id: 9,
    title: '問題⑨:カンマ演算子のトリック',
    code: `let value = (console.log("A"), console.log("B"), 42); console.log(value);`,
    hint: 'カンマ演算子は最後の値を返す。console.log の出力は順に表示される',
  },
  {
    id: 10,
    title: 'チャレンジ:複合(順序注意)',
    code: `let x = 3; let y = 4; let result = x++ * 2 + (--y) ** 2; console.log(result); console.log('x after', x); console.log('y after', y);`,
    hint: '後置・前置、優先順位、べき乗の結合などを意識する',
  }
];

const container = document.getElementById('exercises');
function makeExercise(ex){
  const card = document.createElement('div');
  card.className = 'card';
  card.innerHTML = `
    <h3>${ex.title}</h3>
    <pre class="code">${escapeHtml(ex.code)}</pre>
    <label>予想されるコンソール出力(改行で区切る)</label>
    <textarea id="answer-${ex.id}" placeholder="例: 14"></textarea>
    <div style="margin-top:8px;display:flex;gap:8px">
      <button data-id="${ex.id}" class="check">採点</button>
      <button data-id="${ex.id}" class="hintBtn">ヒントを表示</button>
      <button data-id="${ex.id}" class="explainBtn">解説を表示</button>
    </div>
    <div id="feedback-${ex.id}" class="explain" style="display:none"></div>
  `;
  return card;
}

function escapeHtml(s){ return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }

exercises.forEach(ex => container.appendChild(makeExercise(ex)));

document.getElementById('total').textContent = exercises.length;

// 実行用:与えられたスニペットだけを実行して console.log を捕まえる
function runSnippet(snippet){
  const logs = [];
  const originalLog = console.log;
  try{
    console.log = function(...args){ logs.push(args.map(a=>String(a)).join(' ')); };
    // 評価用に IIFE に包んで安全に実行
    (function(){
      eval(snippet);
    })();
  }catch(e){ logs.push('<<エラー: '+e.message+ '>>'); }
  finally{ console.log = originalLog; }
  return logs;
}

function compareOutputs(userText, actualArr){
  const userLines = userText.split('\n').map(l=>l.trim()).filter(l=>l.length>0);
  const actualLines = actualArr.map(l=>l.trim());
  // 緩めの比較:行数と各行を順に比較
  if(userLines.length !== actualLines.length) return {ok:false, userLines, actualLines};
  for(let i=0;i<userLines.length;i++){
    if(userLines[i] !== actualLines[i]) return {ok:false, userLines, actualLines};
  }
  return {ok:true, userLines, actualLines};
}

// ハンドラ
container.addEventListener('click', (ev)=>{
  const target = ev.target;
  if(target.classList.contains('check')){
    const id = Number(target.dataset.id);
    gradeOne(id);
  }
  if(target.classList.contains('hintBtn')){
    const id = Number(target.dataset.id);
    const fb = document.getElementById('feedback-'+id);
    fb.style.display = 'block';
    fb.innerHTML = `<div class="hint">ヒント: ${exercises.find(e=>e.id===id).hint}</div>`;
  }
  if(target.classList.contains('explainBtn')){
    const id = Number(target.dataset.id);
    showExplain(id);
  }
});

function gradeOne(id){
  const ex = exercises.find(e=>e.id===id);
  const answerEl = document.getElementById('answer-'+id);
  const feedback = document.getElementById('feedback-'+id);
  const user = (answerEl.value || '').trim();
  const actual = runSnippet(ex.code);
  const cmp = compareOutputs(user, actual);
  feedback.style.display = 'block';
  if(cmp.ok){
    feedback.innerHTML = `<div class=\"ok\">✅ 正解</div><div class=\"explain\">実際の出力:<pre>${actual.join('\n')}</pre></div>`;
  } else {
    feedback.innerHTML = `<div class=\"ng\">❌ 不正解</div>
      <div class=\"explain\">あなたの入力:<pre>${cmp.userLines.join('\n')||'<空>'}</pre>
      実際の出力:<pre>${cmp.actualLines.join('\n')}</pre></div>`;
  }
  updateScore();
}

function showExplain(id){
  const ex = exercises.find(e=>e.id===id);
  const fb = document.getElementById('feedback-'+id);
  const actual = runSnippet(ex.code);
  let explanation = '';
  switch(id){
    case 1:
      explanation = '掛け算が先に評価されます。3*4=12、2+12=14。'; break;
    case 2:
      explanation = 'x += 3 -> 8、x *= 2 -> 16。複合代入はその場で計算して再代入します。'; break;
    case 3:
      explanation = '== は型変換して比較するため true。=== は型も比較するため false。'; break;
    case 4:
      explanation = '0 || "default" は 0 が falsy なので "default" を返す。"hello" && 123 は "hello" が truthy なので 123 を返す。null ?? "fallback" は nullish の場合だけ fallback を使う。'; break;
    case 5:
      explanation = 'age >= 18 が true なので "大人" が返る。'; break;
    case 6:
      explanation = 'a は null なので ??= により "default" が代入される。b は 0 なので || = により 10 が代入される。'; break;
    case 7:
      explanation = '** は右結合。3 ** 2 = 9、2 ** 9 = 512。'; break;
    case 8:
      explanation = '代入は右結合。右から評価して全て 5 になる。'; break;
    case 9:
      explanation = 'カンマ演算子はすべての式を評価し最後の値(42)を返す。console.log は実行順に A, B を出力する。'; break;
    case 10:
      explanation = 'x++ は後置なので計算時は 3 を使い、その後 x は 4 に増える。--y は先に減るので y は 3 になり (3)**2 = 9。結果 3*2 + 9 = 15。'; break;
  }
  fb.style.display = 'block';
  fb.innerHTML = `<div class="explain">${explanation}<hr>実際の出力:<pre>${actual.join('\n')}</pre></div>`;
}

function updateScore(){
  let score = 0; let total = exercises.length;
  exercises.forEach(ex=>{
    const user = (document.getElementById('answer-'+ex.id).value||'').trim();
    if(user.length===0) return;
    const actual = runSnippet(ex.code);
    const cmp = compareOutputs(user, actual);
    if(cmp.ok) score++;
  });
  document.getElementById('score').textContent = score;
}

document.getElementById('checkAll').addEventListener('click', ()=>{
  exercises.forEach(ex=>gradeOne(ex.id));
});

document.getElementById('reset').addEventListener('click', ()=>{
  exercises.forEach(ex=>{
    document.getElementById('answer-'+ex.id).value = '';
    const fb = document.getElementById('feedback-'+ex.id);
    fb.style.display = 'none';
    fb.innerHTML = '';
  });
  updateScore();
});

// 初期スコア
updateScore();
</script>
</body>
</html>
HTML

使い方の要点:

  • 各問題の下に予想されるコンソール出力を入力(改行で複数行)して「採点」ボタンを押すと、自動で正誤と実際の出力・解説が表示されます。
  • 「すべて採点」を押すと全問まとめて採点、スコアが表示されます。
  • 「ヒント」「解説」ボタンで段階的に学べます。
タイトルとURLをコピーしました