POS開発 フロントエンド チートコード

HTML / CSS / JavaScript クイックリファレンス

1. HTML 基本

基本構造

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ページタイトル</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- コンテンツ -->
  <script src="app.js"></script>
</body>
</html>

よく使うフォーム要素(POS向け)

<!-- テキスト入力 -->
<input type="text" id="productName" placeholder="商品名">

<!-- 数値入力 -->
<input type="number" id="price" min="0" step="1" placeholder="価格">

<!-- 数量入力 -->
<input type="number" id="quantity" min="1" value="1">

<!-- セレクトボックス -->
<select id="category">
  <option value="">カテゴリを選択</option>
  <option value="food">食品</option>
  <option value="drink">飲料</option>
</select>

<!-- ボタン -->
<button type="button" id="addBtn">カートに追加</button>
<button type="submit">会計する</button>

テーブル(明細表示)

<table>
  <thead>
    <tr>
      <th>商品名</th><th>単価</th><th>数量</th><th>小計</th>
    </tr>
  </thead>
  <tbody id="cartBody">
    <!-- JSで動的に追加 -->
  </tbody>
</table>

data属性

<!-- HTML側 -->
<button data-product-id="101" data-price="500">商品A</button>

// JS側で取得
const btn = document.querySelector('button');
btn.dataset.productId;  // "101"  ※ケバブケース→キャメルケースに自動変換
btn.dataset.price;      // "500"  ※常に文字列で返る → Number()で変換

2. CSS 基本

セレクタ

/* 基本セレクタ */
.class-name { }           /* クラス */
#id-name { }              /* ID */
div p { }                  /* 子孫 */
div > p { }                /* 直接の子 */
div + p { }                /* 隣接兄弟 */

/* 擬似クラス */
button:hover { }           /* ホバー時 */
input:focus { }            /* フォーカス時 */
tr:nth-child(even) { }     /* 偶数行 */
li:first-child { }         /* 最初の子 */
li:last-child { }          /* 最後の子 */

/* 属性セレクタ */
[data-active="true"] { }   /* data属性で絞り込み */

Flexbox レイアウト

/* 横並び(中央揃え) */
.container {
  display: flex;
  justify-content: center;     /* 主軸: center | space-between | space-around */
  align-items: center;         /* 交差軸: center | flex-start | flex-end */
  gap: 16px;                    /* 要素間の隙間 */
}

/* 折り返し */
.wrap-container {
  display: flex;
  flex-wrap: wrap;
}

/* 子要素の伸縮 */
.item {
  flex: 1;                      /* 均等に広がる */
  flex: 0 0 200px;              /* 固定幅200px */
}

Grid レイアウト

/* 3列グリッド */
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);  /* 等幅3列 */
  gap: 16px;
}

/* 自動フィット(レスポンシブ) */
.auto-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
}

よく使うプロパティ

/* ボックスモデル */
margin: 16px;                  /* 外側の余白 */
padding: 12px 16px;            /* 内側の余白(上下 左右) */
border: 1px solid #ddd;
border-radius: 8px;

/* テキスト */
font-size: 16px;
font-weight: bold;
color: #333;
text-align: center;

/* 背景・影 */
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);

/* トランジション */
transition: all 0.3s ease;

レスポンシブ(Media Query)

/* モバイル: 640px以下 */
@media (max-width: 640px) {
  .container { flex-direction: column; }
}

/* タブレット以上 */
@media (min-width: 768px) {
  .sidebar { display: block; }
}

3. JavaScript 基本文法

変数宣言

const TAX_RATE = 0.1;       // 再代入不可(基本はconstを使う)
let total = 0;               // 再代入可能
// var は使わない(スコープの問題があるため)

データ型・型変換

// ── 型の確認 ──
typeof "hello"     // "string"
typeof 42          // "number"
typeof true        // "boolean"
typeof undefined   // "undefined"
typeof null        // "object"  ← 歴史的バグ、nullチェックには === null を使う
Array.isArray([])  // true

// ── 型変換 ──
Number("123")      // 123
Number("abc")      // NaN
Number("")         // 0   ← 空文字は0になるので注意
parseInt("100円")  // 100  ← 先頭の数値部分だけ変換
parseFloat("3.14")// 3.14
String(500)        // "500"
Boolean(0)         // false  (0, "", null, undefined, NaN → false)
Boolean("text")    // true   (それ以外は全てtrue)

// ── NaNの判定 ──
Number.isNaN(value)  // NaNかどうか(isNaN()よりこちらを推奨)
Number.isFinite(42)  // true(有限の数値かどうか)

比較演算子

// === 厳密等価(型も値も一致)← 基本はこちらを使う
1 === 1        // true
1 === "1"      // false(型が違う)

// == 抽象等価(型変換してから比較)← バグの元なので避ける
1 == "1"       // true
0 == false     // true
null == undefined // true

// 大小比較
5 > 3          // true
5 >= 5         // true

論理演算子

// AND / OR / NOT
true && false    // false
true || false    // true
!true            // false

// 短絡評価(ショートサーキット)
const name = user && user.name;   // userがtruthyならuser.nameを返す
const value = input || "デフォルト"; // inputがfalsyなら右辺を返す

// Null合体演算子 ??(null/undefinedのみ右辺を返す)
const qty = order.quantity ?? 1;
// || との違い: 0 || 1 → 1,  0 ?? 1 → 0(0はnullでもundefinedでもない)

条件分岐

// if - else if - else
if (price >= 10000) {
  discount = 0.1;
} else if (price >= 5000) {
  discount = 0.05;
} else {
  discount = 0;
}

// 三項演算子
const label = stock > 0 ? "在庫あり" : "在庫切れ";

// switch
switch (paymentMethod) {
  case "cash":   handleCash();  break;
  case "card":   handleCard();  break;
  case "ic":     handleIC();    break;
  default:       handleOther();
}

// オプショナルチェーン ?.
const city = user?.address?.city;       // 途中がnull/undefinedでもエラーにならない
const len  = items?.length ?? 0;        // ?.と??の組み合わせ

ループ

const items = ["りんご", "バナナ", "みかん"];

// for(インデックスが必要な場合)
for (let i = 0; i < items.length; i++) {
  console.log(`${i}: ${items[i]}`);
}

// for...of(配列の要素を順に取得、最も読みやすい)
for (const item of items) {
  console.log(item);
}

// forEach(コールバック関数で処理)
items.forEach((item, index) => {
  console.log(`${index}: ${item}`);
});

// for...in(オブジェクトのキーを列挙)
const prices = { apple: 100, banana: 200 };
for (const key in prices) {
  console.log(`${key}: ${prices[key]}`);
}

// while
let count = 0;
while (count < 5) {
  console.log(count);
  count++;
}

// break / continue
for (const item of items) {
  if (item === "バナナ") continue;  // スキップして次へ
  if (item === "みかん") break;     // ループを終了
}

関数

// 関数宣言(巻き上げ: 宣言前に呼び出し可能)
function calcTax(price, rate = 0.1) {
  return Math.floor(price * rate);
}

// アロー関数(1行なら return 省略可)
const calcTax = (price, rate = 0.1) => Math.floor(price * rate);

// 複数行のアロー関数
const calcTotal = (items) => {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  const tax = Math.floor(subtotal * 0.1);
  return { subtotal, tax, total: subtotal + tax };
};

// 即時実行関数 (IIFE)
(() => {
  const private = "外からアクセスできない";
})();

テンプレートリテラル

const name = "商品A";
const price = 1500;

// 変数埋め込み
const msg = `${name}の価格は${price}円です`;

// 式の埋め込み
const result = `税込: ¥${Math.floor(price * 1.1).toLocaleString()}`;

// 複数行HTML生成
const html = `
  <div class="item">
    <span class="name">${name}</span>
    <span class="price">¥${price.toLocaleString()}</span>
  </div>
`;

分割代入・スプレッド構文

// ── オブジェクトの分割代入 ──
const product = { name: "コーヒー", price: 350, stock: 20 };
const { name, price } = product;

// リネーム付き(変数名の衝突を避ける)
const { name: productName, price: unitPrice } = product;

// デフォルト値
const { discount = 0 } = product;  // なければ0

// ネストした分割代入
const order = { customer: { name: "田中" } };
const { customer: { name: customerName } } = order;

// ── 配列の分割代入 ──
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// first=1, second=2, rest=[3,4,5]

// 値のスワップ
let a = 1, b = 2;
[a, b] = [b, a];  // a=2, b=1

// ── スプレッド構文 ──
const newItems = [...items, "新商品"];             // 配列に追加
const updated = { ...product, price: 400 };       // オブジェクトの一部上書き
const merged  = { ...defaults, ...userConfig };  // オブジェクトのマージ

// 関数の引数に展開
const nums = [3, 1, 4, 1, 5];
Math.max(...nums);  // 5

三項演算子 vs 論理演算 使い分け

// 2つの値から選ぶ → 三項演算子
const label = isActive ? "有効" : "無効";

// デフォルト値の設定 → ?? (null/undefined) か || (全falsy)
const qty = inputValue ?? 1;

// 条件付きで実行 → &&
isLoggedIn && showDashboard();

// 条件付きでプロパティを含める
const payload = {
  name,
  price,
  ...(discount > 0 && { discount }),  // discountが0より大きい場合のみ含める
};

4. 文字列・数値操作

文字列メソッド

const str = "  Hello World  ";

// 検索
str.includes("World")       // true   含まれるか
str.startsWith("  Hello")  // true   先頭一致
str.endsWith("  ")          // true   末尾一致
str.indexOf("World")        // 8      位置(見つからない場合-1)

// 変換
str.trim()                   // "Hello World"  前後の空白除去
str.toUpperCase()             // "  HELLO WORLD  "
str.toLowerCase()             // "  hello world  "

// 切り出し
str.slice(2, 7)              // "Hello"   開始位置〜終了位置(終了は含まない)
str.slice(-7)                // "orld  "  後ろから

// 置換
str.replace("World", "JS")  // "  Hello JS  "  最初の1つ
str.replaceAll(" ", "")      // "HelloWorld"    全て置換

// 分割・結合
"a,b,c".split(",")           // ["a", "b", "c"]
["a", "b", "c"].join("-")    // "a-b-c"

// パディング(桁揃え)
"5".padStart(3, "0")         // "005"  レシート番号等に便利
"Hi".padEnd(10, ".")         // "Hi........"

// 繰り返し
"-".repeat(30)                // "------------------------------"

数値・Math

// 丸め
Math.floor(3.7)       // 3    切り捨て(税計算でよく使う)
Math.ceil(3.2)        // 4    切り上げ
Math.round(3.5)       // 4    四捨五入
Math.trunc(3.9)       // 3    小数部除去

// 最大・最小・絶対値
Math.max(1, 5, 3)      // 5
Math.min(1, 5, 3)      // 1
Math.abs(-42)          // 42

// ランダム
Math.random()           // 0以上1未満のランダムな小数
Math.floor(Math.random() * 100)  // 0〜99のランダムな整数

// 表示用フォーマット
(1500).toLocaleString()           // "1,500"  3桁区切り
(0.1).toFixed(2)                   // "0.10"   小数点以下2桁
(1500).toLocaleString('ja-JP', { style: 'currency', currency: 'JPY' })
// "¥1,500"  通貨フォーマット

日付(Date)

// 現在日時の取得
const now = new Date();
now.getFullYear()   // 2026
now.getMonth()      // 0-11(0=1月 ← 注意)
now.getDate()       // 1-31(日)
now.getDay()        // 0-6(0=日曜)
now.getHours()      // 0-23
now.getMinutes()    // 0-59

// 日付の生成
new Date('2026-04-10')          // ISO形式
new Date(2026, 3, 10)           // 年, 月(0始まり), 日

// 表示用フォーマット
now.toLocaleDateString('ja-JP')
// "2026/4/10"

now.toLocaleString('ja-JP')
// "2026/4/10 14:30:00"

now.toLocaleDateString('ja-JP', {
  year: 'numeric', month: '2-digit', day: '2-digit',
  weekday: 'short'
})
// "2026/04/10(金)"

// ISO文字列(APIに送る場合)
now.toISOString()  // "2026-04-10T05:30:00.000Z"

// タイムスタンプ(ミリ秒)
Date.now()          // 現在のタイムスタンプ
now.getTime()       // Dateオブジェクトからタイムスタンプ

// 日付計算(ミリ秒単位で加減算)
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);

// setDateを使った加算
const nextWeek = new Date(now);
nextWeek.setDate(nextWeek.getDate() + 7);

正規表現

// 基本パターン
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
emailRegex.test("user@example.com")  // true

// 電話番号チェック
const phoneRegex = /^0\d{1,4}-?\d{1,4}-?\d{4}$/;
phoneRegex.test("090-1234-5678")   // true

// 半角数字のみ
/^\d+$/.test("12345")               // true

// matchで取り出し
const str = "注文番号: ORD-12345";
const match = str.match(/ORD-(\d+)/);
match[1]  // "12345"

// replaceで変換
"2026-04-10".replace(/(\d{4})-(\d{2})-(\d{2})/, "$1年$2月$3日")
// "2026年04月10日"

5. DOM 操作

要素の取得

// 単一要素の取得
const el = document.querySelector('.class-name');    // CSSセレクタで最初の1つ
const el = document.getElementById('myId');          // IDで取得(#不要)

// 複数要素の取得(NodeList → forEachが使える)
const items = document.querySelectorAll('.item');
items.forEach(item => { /* ... */ });

// NodeListを配列に変換(map/filterを使いたい場合)
const arr = [...document.querySelectorAll('.item')];
const arr = Array.from(document.querySelectorAll('.item'));

// 親・子・兄弟の参照
el.parentElement;            // 親要素
el.children;                 // 子要素一覧(HTMLCollection)
el.firstElementChild;        // 最初の子要素
el.lastElementChild;         // 最後の子要素
el.nextElementSibling;       // 次の兄弟要素
el.previousElementSibling;   // 前の兄弟要素
el.closest('.wrapper');       // 祖先を遡って検索

要素の作成・追加・削除

// 要素の作成
const div = document.createElement('div');
div.textContent = "テキスト";                // テキストのみ設定(安全)
div.innerHTML = '<span>HTML</span>';       // HTMLとして設定(XSS注意)

// 追加
parent.appendChild(div);             // 末尾に追加
parent.prepend(div);                  // 先頭に追加
parent.insertBefore(div, reference);  // 指定要素の前に追加
el.after(newEl);                       // 要素の直後に追加
el.before(newEl);                      // 要素の直前に追加

// insertAdjacentHTML(文字列HTMLを位置指定で挿入)
// 位置: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'
el.insertAdjacentHTML('beforeend', `
  <tr>
    <td>${name}</td>
    <td>¥${price.toLocaleString()}</td>
  </tr>
`);

// 削除
el.remove();                          // 自身を削除
parent.removeChild(child);             // 子要素を削除
parent.innerHTML = '';                // 子要素を全削除

// 要素の置換
parent.replaceChild(newEl, oldEl);

クラス操作

el.classList.add('active');         // 追加
el.classList.add('a', 'b');         // 複数追加
el.classList.remove('active');      // 削除
el.classList.toggle('active');      // 切替(あれば削除、なければ追加)
el.classList.contains('active');    // 存在チェック → true/false
el.classList.replace('old', 'new'); // 置換

スタイル・属性操作

// インラインスタイル
el.style.color = 'red';
el.style.display = 'none';
el.style.backgroundColor = '#f0f0f0';  // ケバブ→キャメルケース

// 計算済みスタイルの取得
const styles = getComputedStyle(el);
styles.width;  // "200px"

// 属性の取得・設定
el.getAttribute('data-id');
el.setAttribute('disabled', 'true');
el.removeAttribute('disabled');
el.hasAttribute('disabled');    // true/false

// フォーム値の取得・設定
const input = document.getElementById('price');
input.value;            // 取得(文字列として)
input.value = "1000";   // 設定
input.disabled = true;  // プロパティとして直接設定も可

イベントリスナー

// クリック
btn.addEventListener('click', () => {
  console.log('クリックされた');
});

// 入力変更(リアルタイム、キー入力ごとに発火)
input.addEventListener('input', (e) => {
  console.log(e.target.value);
});

// 値の確定(フォーカスが外れた時、Enterキーなど)
input.addEventListener('change', (e) => {
  console.log(e.target.value);
});

// フォーム送信
form.addEventListener('submit', (e) => {
  e.preventDefault();  // ページリロードを防止
  const formData = new FormData(form);
  console.log(Object.fromEntries(formData));
});

// キーボード
document.addEventListener('keydown', (e) => {
  if (e.key === 'Enter')    { /* Enter処理 */ }
  if (e.key === 'Escape')   { /* モーダル閉じるなど */ }
  if (e.key === 'F2')       { /* ファンクションキー */ }
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault();  // ブラウザのデフォルトを防止
    saveData();
  }
});

// リスナーの削除(参照を保持しておく必要がある)
const handler = () => console.log('click');
btn.addEventListener('click', handler);
btn.removeEventListener('click', handler);

// 一度だけ実行するリスナー
btn.addEventListener('click', handler, { once: true });

イベント委譲

// 親要素にリスナーを1つだけ設定(動的に追加された子要素にも対応)
document.getElementById('cartBody').addEventListener('click', (e) => {
  // 削除ボタン
  const deleteBtn = e.target.closest('.delete-btn');
  if (deleteBtn) {
    const row = deleteBtn.closest('tr');
    row.remove();
    return;
  }

  // 数量変更ボタン
  const qtyBtn = e.target.closest('.qty-btn');
  if (qtyBtn) {
    const action = qtyBtn.dataset.action;  // "increase" or "decrease"
    updateQuantity(qtyBtn.closest('tr'), action);
  }
});

DOMContentLoaded / ページ読み込み

// DOM構築完了後に実行(画像読み込みを待たない)
document.addEventListener('DOMContentLoaded', () => {
  init();
});

// 全リソース読み込み完了後に実行
window.addEventListener('load', () => {
  hideLoadingScreen();
});

// scriptタグにdefer属性を付ければDOMContentLoadedは不要
// <script src="app.js" defer></script>

6. 配列・オブジェクト

配列メソッド

const products = [
  { name: "コーヒー", price: 350, category: "drink" },
  { name: "サンド",   price: 500, category: "food" },
  { name: "紅茶",     price: 300, category: "drink" },
  { name: "ケーキ",   price: 450, category: "food" },
];

// map: 変換して新しい配列を返す
const names = products.map(p => p.name);
// ["コーヒー", "サンド", "紅茶", "ケーキ"]

// filter: 条件に合う要素だけ抽出
const drinks = products.filter(p => p.category === "drink");

// find: 条件に合う最初の1つを返す(見つからなければundefined)
const coffee = products.find(p => p.name === "コーヒー");

// findIndex: 条件に合う最初のインデックス(見つからなければ-1)
const idx = products.findIndex(p => p.name === "紅茶");  // 2

// reduce: 集約(合計計算など)
const total = products.reduce((sum, p) => sum + p.price, 0);  // 1600

// sort: 並べ替え(※元の配列を変更する → スプレッドでコピー推奨)
const sorted = [...products].sort((a, b) => a.price - b.price);  // 安い順
const desc   = [...products].sort((a, b) => b.price - a.price);  // 高い順

// some: 1つでも条件を満たすか
products.some(p => p.price >= 500);  // true

// every: 全て条件を満たすか
products.every(p => p.price < 1000);  // true

// includes: 含まれるか(プリミティブ配列向け)
["cash", "card"].includes("cash");  // true

// indexOf: 要素のインデックスを返す(見つからなければ-1)
["cash", "card", "ic"].indexOf("card");  // 1
["cash", "card", "ic"].indexOf("qr");    // -1

// join: 配列を文字列に結合
["コーヒー", "サンド", "紅茶"].join("、");  // "コーヒー、サンド、紅茶"
["2024", "01", "15"].join("-");             // "2024-01-15"

配列の追加・削除・変換

const arr = [1, 2, 3];

// 追加・削除(元の配列を変更する)
arr.push(4);        // 末尾に追加   → [1,2,3,4]
arr.pop();          // 末尾を削除   → [1,2,3]  ※削除した値を返す
arr.unshift(0);     // 先頭に追加   → [0,1,2,3]
arr.shift();        // 先頭を削除   → [1,2,3]  ※削除した値を返す

// splice: 指定位置で削除/挿入(元の配列を変更する)
arr.splice(1, 1);          // index1から1個削除  → [1,3]
arr.splice(1, 0, 10, 20); // index1に挿入       → [1,10,20,3]

// 元の配列を変更しない操作(イミュータブル)
const newArr = [...arr, 4];                       // 末尾に追加
const without = arr.filter((_, i) => i !== 1);   // index1を除外
const sliced = arr.slice(0, 2);                  // index0〜1を抽出

// flat: ネスト配列を平坦化
[[1,2], [3,4]].flat()    // [1,2,3,4]

// flatMap: mapしてからflatする
orders.flatMap(o => o.items)  // 全注文の商品を1つの配列に

// 重複排除
const unique = [...new Set([1,2,2,3])];  // [1,2,3]

メソッドチェーン

// filter → map → sort を連結
const result = products
  .filter(p => p.category === "drink")     // 飲料だけ
  .map(p => ({ ...p, taxIncluded: Math.floor(p.price * 1.1) }))  // 税込価格追加
  .sort((a, b) => a.price - b.price);     // 安い順

// reduce でグループ化
const grouped = products.reduce((acc, p) => {
  (acc[p.category] ??= []).push(p);
  return acc;
}, {});
// { drink: [...], food: [...] }

オブジェクト操作

const product = { name: "コーヒー", price: 350, stock: 20 };

// キー・値・エントリの取得
Object.keys(product);     // ["name", "price", "stock"]
Object.values(product);   // ["コーヒー", 350, 20]
Object.entries(product);  // [["name","コーヒー"], ["price",350], ...]

// エントリからオブジェクトに変換
Object.fromEntries([
  ["name", "コーヒー"],
  ["price", 350]
]);  // { name: "コーヒー", price: 350 }

// プロパティの存在チェック
"name" in product;    // true

// 動的キー
const key = "price";
product[key];  // 350

// 動的キーでオブジェクト生成
const field = "category";
const obj = { [field]: "drink" };  // { category: "drink" }

JSON操作

// オブジェクト → JSON文字列
const json = JSON.stringify(product);
// '{"name":"コーヒー","price":350,"stock":20}'

// 整形出力(デバッグ用)
console.log(JSON.stringify(product, null, 2));

// JSON文字列 → オブジェクト
const obj = JSON.parse(json);

// ディープコピー(ネストしたオブジェクトのコピー)
const copy = JSON.parse(JSON.stringify(original));
// ※Date, function, undefinedは失われる。より正確にはstructuredClone()を使う:
const copy = structuredClone(original);

Map / Set

// ── Map: キーが何でも使えるハッシュマップ ──
const cart = new Map();
cart.set("product_101", { name: "コーヒー", qty: 2 });
cart.get("product_101");   // { name: "コーヒー", qty: 2 }
cart.has("product_101");   // true
cart.delete("product_101");
cart.size;                  // 要素数

for (const [key, value] of cart) {
  console.log(key, value);
}

// ── Set: 重複のないコレクション ──
const categories = new Set(["food", "drink", "food"]);
// Set(2) { "food", "drink" }
categories.add("snack");
categories.has("food");    // true
categories.delete("food");
[...categories];             // 配列に変換

7. 非同期処理

コールバック → Promise → async/await の流れ

JavaScriptはシングルスレッドのため、時間のかかる処理(API呼び出し、タイマーなど)は非同期で実行される。async/awaitが最も読みやすく推奨。

setTimeout / setInterval

// 一定時間後に1回実行
const timerId = setTimeout(() => {
  console.log("3秒後に実行");
}, 3000);
clearTimeout(timerId);  // キャンセル

// 一定間隔で繰り返し実行
const intervalId = setInterval(() => {
  console.log("5秒ごとに実行");
}, 5000);
clearInterval(intervalId);  // 停止

Promise 基本

// Promiseの作成
const myPromise = new Promise((resolve, reject) => {
  const success = true;
  if (success) {
    resolve("成功データ");   // 成功時
  } else {
    reject(new Error("エラー"));  // 失敗時
  }
});

// then / catch / finally チェーン
myPromise
  .then(data => {
    console.log(data);       // "成功データ"
    return processData(data); // 次のthenに値を渡す
  })
  .then(result => {
    console.log(result);     // processDataの結果
  })
  .catch(err => {
    console.error(err);      // どこかでエラーが起きたら
  })
  .finally(() => {
    console.log("完了");      // 成功でも失敗でも実行
  });

async / await

// async関数: Promiseを返す関数を宣言
async function loadProducts() {
  const response = await fetch('/api/products');  // Promiseの完了を待つ
  const data = await response.json();              // これもPromise
  return data;
}

// アロー関数版
const loadProducts = async () => {
  const res = await fetch('/api/products');
  return res.json();
};

// 呼び出し側
const products = await loadProducts();  // トップレベルawait(モジュール内)

// または
loadProducts().then(products => {
  renderProducts(products);
});

エラーハンドリング

async function checkout(cartItems) {
  const loading = showLoading();
  try {
    const result = await submitOrder(cartItems);
    showSuccess("注文が完了しました");
    return result;
  } catch (error) {
    // エラーの種類で分岐
    if (error instanceof TypeError) {
      showError("ネットワークエラー: サーバーに接続できません");
    } else if (error.status === 400) {
      showError("入力内容に問題があります");
    } else {
      showError(`予期しないエラー: ${error.message}`);
    }
  } finally {
    hideLoading(loading);  // 成功でも失敗でも実行される
  }
}

Promise.all / Promise.allSettled / Promise.race

// Promise.all: 全て成功するまで待つ(1つでも失敗したら即catch)
try {
  const [users, products, orders] = await Promise.all([
    fetchUsers(),
    fetchProducts(),
    fetchOrders(),
  ]);
} catch (e) {
  // いずれか1つが失敗した場合
}

// Promise.allSettled: 全ての結果を取得(失敗しても止まらない)
const results = await Promise.allSettled([
  fetchUsers(),
  fetchProducts(),
]);
results.forEach(r => {
  if (r.status === "fulfilled") console.log(r.value);
  if (r.status === "rejected")  console.error(r.reason);
});

// Promise.race: 最初に完了(成功or失敗)したものを採用
const result = await Promise.race([
  fetchData(),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error("タイムアウト")), 5000)
  ),
]);

非同期の逐次実行 vs 並列実行

// ❌ 逐次実行(遅い: A完了 → B完了 → C完了)
const a = await fetchA();
const b = await fetchB();
const c = await fetchC();

// ✅ 並列実行(速い: A,B,Cを同時に開始)
const [a, b, c] = await Promise.all([
  fetchA(),
  fetchB(),
  fetchC(),
]);

// 配列の各要素を順番に非同期処理する場合
for (const item of items) {
  await processItem(item);  // 1つずつ順番に
}

8. Fetch API

GETリクエスト

// 基本的なGET
const response = await fetch('/api/products');
const data = await response.json();

// クエリパラメータ付き
const params = new URLSearchParams({
  category: "drink",
  limit: "10",
  page: "1",
});
const res = await fetch(`/api/products?${params}`);
const drinks = await res.json();

POSTリクエスト(JSON送信)

const response = await fetch('/api/orders', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    items: cartItems,
    paymentMethod: "cash",
    total: 2500,
  }),
});

const result = await response.json();

PUT / DELETE

// PUT: 更新
await fetch(`/api/products/${id}`, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ price: newPrice }),
});

// DELETE: 削除
await fetch(`/api/products/${id}`, {
  method: 'DELETE',
});

レスポンスの処理

const response = await fetch(url);

// ステータス確認
response.ok;         // true (200-299の場合)
response.status;     // 200, 404, 500 など
response.statusText; // "OK", "Not Found" など

// ボディの取得(※1回しか読めない)
const json = await response.json();   // JSONとしてパース
const text = await response.text();   // テキストとして取得
const blob = await response.blob();   // バイナリ(画像など)

// レスポンスヘッダーの確認
response.headers.get('Content-Type');

エラーハンドリング

fetchは404や500でもrejectしない(ネットワークエラーのみreject)。response.okのチェックが必須。
async function apiFetch(url, options = {}) {
  try {
    const response = await fetch(url, options);

    if (!response.ok) {
      // HTTPエラー(404, 500など)→ fetchはrejectしないので自分でthrow
      const errorBody = await response.text();
      throw new Error(`HTTP ${response.status}: ${errorBody}`);
    }

    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      // ネットワークエラー(サーバー未到達、DNS解決失敗など)
      console.error("ネットワークエラー:", error.message);
    } else {
      // HTTPエラーまたはJSONパースエラー
      console.error("APIエラー:", error.message);
    }
    throw error;
  }
}

FormDataで送信

// フォームから直接取得して送信
const form = document.getElementById('myForm');
const formData = new FormData(form);

// Content-Typeは自動設定されるので指定不要
await fetch('/api/upload', {
  method: 'POST',
  body: formData,
});

// FormDataの中身を確認
for (const [key, value] of formData) {
  console.log(`${key}: ${value}`);
}

// FormDataをオブジェクトに変換
const obj = Object.fromEntries(formData);

AbortController(リクエストのキャンセル)

const controller = new AbortController();

// タイムアウト付きfetch
const timeoutId = setTimeout(() => controller.abort(), 5000);

try {
  const res = await fetch('/api/data', {
    signal: controller.signal,
  });
  clearTimeout(timeoutId);
  return await res.json();
} catch (e) {
  if (e.name === 'AbortError') {
    console.log('リクエストがキャンセルされました');
  }
}

// 検索入力時の前回リクエストキャンセル
let currentController = null;
input.addEventListener('input', async (e) => {
  currentController?.abort();  // 前回のリクエストをキャンセル
  currentController = new AbortController();
  try {
    const res = await fetch(`/api/search?q=${e.target.value}`, {
      signal: currentController.signal,
    });
    const data = await res.json();
    renderResults(data);
  } catch (e) {
    if (e.name !== 'AbortError') throw e;
  }
});

9. 実用パターン

POS向け API呼び出しラッパー

const API_BASE = '/api';

const api = {
  async getProducts() {
    const res = await fetch(`${API_BASE}/products`);
    if (!res.ok) throw new Error("商品の取得に失敗");
    return res.json();
  },

  async createOrder(orderData) {
    const res = await fetch(`${API_BASE}/orders`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(orderData),
    });
    if (!res.ok) throw new Error("注文の送信に失敗");
    return res.json();
  },

  async getSales(date) {
    const params = new URLSearchParams({ date });
    const res = await fetch(`${API_BASE}/sales?${params}`);
    if (!res.ok) throw new Error("売上の取得に失敗");
    return res.json();
  },
};

デバウンス(検索入力の最適化)

// 入力が止まってから指定ms後に実行(API呼び出しを抑制)
function debounce(fn, delay = 300) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// 使い方
const searchProducts = debounce(async (query) => {
  const results = await api.search(query);
  renderResults(results);
}, 300);

searchInput.addEventListener('input', (e) => {
  searchProducts(e.target.value);
});

金額フォーマット

// 通貨表示
const formatYen = (amount) =>
  `¥${amount.toLocaleString()}`;

formatYen(15800);  // "¥15,800"

// Intl.NumberFormatを使う(より正式)
const formatter = new Intl.NumberFormat('ja-JP', {
  style: 'currency',
  currency: 'JPY',
});
formatter.format(15800);  // "¥15,800"

// 税込計算
const withTax = (price, rate = 0.1) => Math.floor(price * (1 + rate));
const taxOf   = (price, rate = 0.1) => Math.floor(price * rate);

テーブル行の動的生成

function renderCartTable(items) {
  const tbody = document.getElementById('cartBody');
  tbody.innerHTML = '';  // 一旦クリア

  items.forEach((item, i) => {
    const subtotal = item.price * item.qty;
    tbody.insertAdjacentHTML('beforeend', `
      <tr data-index="${i}">
        <td>${item.name}</td>
        <td>¥${item.price.toLocaleString()}</td>
        <td>
          <button class="qty-btn" data-action="decrease">-</button>
          <span>${item.qty}</span>
          <button class="qty-btn" data-action="increase">+</button>
        </td>
        <td>¥${subtotal.toLocaleString()}</td>
        <td><button class="delete-btn">削除</button></td>
      </tr>
    `);
  });

  // 合計表示
  const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  document.getElementById('total').textContent = formatYen(total);
}

モーダル表示

// HTML: <dialog id="confirmDialog">...</dialog>

// dialog要素を使ったモーダル(ネイティブ対応)
const dialog = document.getElementById('confirmDialog');

// 表示
dialog.showModal();    // モーダル表示(背景クリック不可)
dialog.show();         // 非モーダル表示

// 閉じる
dialog.close();

// 閉じた時の処理
dialog.addEventListener('close', () => {
  console.log(dialog.returnValue);  // formのsubmitで設定された値
});

// ESCキーで閉じるのを防止
dialog.addEventListener('cancel', (e) => {
  e.preventDefault();
});

エラー表示ユーティリティ

function showToast(message, type = 'info') {
  const toast = document.createElement('div');
  toast.className = `toast toast-${type}`;
  toast.textContent = message;
  document.body.appendChild(toast);

  // アニメーション後に自動削除
  setTimeout(() => {
    toast.classList.add('fade-out');
    toast.addEventListener('animationend', () => toast.remove());
  }, 3000);
}

// 使い方
showToast('商品を追加しました', 'success');
showToast('在庫が不足しています', 'error');

10. クラス構文とOOP 中級

Class基本(constructor / メソッド / getter・setter)

class CartItem {
  constructor(name, price, quantity = 1) {
    this.name = name;
    this.price = price;
    this.quantity = quantity;
  }

  // メソッド
  subtotal() {
    return this.price * this.quantity;
  }

  // getter — プロパティのようにアクセスできる
  get taxIncluded() {
    return Math.floor(this.subtotal() * 1.1);
  }

  // setter — 代入で呼ばれる
  set qty(value) {
    if (value < 1) throw new Error('数量は1以上');
    this.quantity = value;
  }
}

const item = new CartItem('コーヒー', 350, 2);
console.log(item.subtotal());    // 700
console.log(item.taxIncluded);    // 770
item.qty = 3;                    // setter経由

継承(extends / super)

class DiscountedItem extends CartItem {
  constructor(name, price, quantity, discountRate) {
    super(name, price, quantity);  // 親のconstructorを呼ぶ
    this.discountRate = discountRate;
  }

  // メソッドのオーバーライド
  subtotal() {
    const base = super.subtotal();  // 親のメソッドを呼ぶ
    return Math.floor(base * (1 - this.discountRate));
  }
}

const sale = new DiscountedItem('弁当', 500, 1, 0.2);
console.log(sale.subtotal());     // 400(20%引き)
console.log(sale instanceof CartItem);  // true

プライベートフィールド(#)

class PaymentProcessor {
  // # で始まるフィールドはクラス外からアクセス不可
  #apiKey;
  #endpoint;

  constructor(apiKey, endpoint) {
    this.#apiKey = apiKey;
    this.#endpoint = endpoint;
  }

  async charge(amount) {
    const res = await fetch(this.#endpoint, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.#apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ amount }),
    });
    return res.json();
  }
}

const payment = new PaymentProcessor('sk_xxx', '/api/pay');
// payment.#apiKey → SyntaxError(外部からアクセス不可)

静的メソッド(static)

class CartItem {
  constructor(name, price, quantity) {
    this.name = name;
    this.price = price;
    this.quantity = quantity;
  }

  // インスタンスを作らずに呼べるファクトリメソッド
  static fromBarcode(barcode, catalog) {
    const product = catalog.find(p => p.barcode === barcode);
    if (!product) throw new Error(`商品が見つかりません: ${barcode}`);
    return new CartItem(product.name, product.price, 1);
  }

  // 複数アイテムの合計を計算するユーティリティ
  static totalOf(items) {
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }
}

// 使い方
const item = CartItem.fromBarcode('4901234567890', catalog);
const total = CartItem.totalOf(cartItems);

11. エラーハンドリング応用 中級

カスタムErrorクラス

class AppError extends Error {
  constructor(message, code) {
    super(message);
    this.name = this.constructor.name;
    this.code = code;
  }
}

class PaymentError extends AppError {
  constructor(message, code, transactionId) {
    super(message, code);
    this.transactionId = transactionId;
  }
}

class StockError extends AppError {}

// 使い分け
try {
  throw new PaymentError('決済失敗', 'PAY_001', 'txn_abc');
} catch (e) {
  if (e instanceof PaymentError) {
    console.log(`決済エラー [${e.code}]: ${e.message}`);
  } else if (e instanceof StockError) {
    console.log(`在庫エラー: ${e.message}`);
  } else {
    throw e;  // 未知のエラーは再throw
  }
}

リトライ(指数バックオフ)

async function retryFetch(url, options = {}, maxRetries = 3) {
  let lastError;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const res = await fetch(url, options);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.json();
    } catch (err) {
      lastError = err;
      if (attempt < maxRetries - 1) {
        // 待機時間: 1秒 → 2秒 → 4秒...
        const delay = 1000 * Math.pow(2, attempt);
        console.log(`リトライ ${attempt + 1}/${maxRetries}(${delay}ms後)`);
        await new Promise(r => setTimeout(r, delay));
      }
    }
  }

  throw new Error(`${maxRetries}回リトライ後も失敗: ${lastError.message}`);
}

// 使い方
const data = await retryFetch('/api/orders');

グローバルエラーハンドリング

// 同期エラーのキャッチ
window.addEventListener('error', (e) => {
  console.error('未処理エラー:', e.message, e.filename, e.lineno);
  // サーバーにエラーレポートを送信
  fetch('/api/error-log', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      message: e.message,
      source: e.filename,
      line: e.lineno,
      time: new Date().toISOString(),
    }),
  });
});

// 未処理のPromise rejectionのキャッチ
window.addEventListener('unhandledrejection', (e) => {
  console.error('未処理Promise:', e.reason);
  e.preventDefault();  // コンソールのデフォルトエラー表示を抑制
});

エラー境界パターン(操作全体をラップ)

async function safePay(orderId, amount) {
  try {
    // 1. 在庫チェック
    await checkStock(orderId);
    // 2. 決済実行
    const result = await processPayment(orderId, amount);
    // 3. 完了通知
    showToast('決済完了', 'success');
    return result;

  } catch (e) {
    if (e instanceof StockError) {
      showToast(`在庫不足: ${e.message}`, 'error');
    } else if (e instanceof PaymentError) {
      showToast(`決済失敗: ${e.message}`, 'error');
      // 決済ロールバック
      await rollbackPayment(e.transactionId);
    } else {
      showToast('予期しないエラーが発生しました', 'error');
    }
    console.error('safePay失敗:', e);
    return null;
  }
}

12. フォームバリデーション 中級

Constraint Validation API

// HTML側: <input type="number" id="price" min="0" max="999999" required>
const priceInput = document.getElementById('price');

// 標準バリデーション
priceInput.checkValidity();    // true / false
priceInput.reportValidity();   // ブラウザのツールチップを表示

// カスタムバリデーション
priceInput.addEventListener('input', () => {
  const value = Number(priceInput.value);

  if (value % 10 !== 0) {
    priceInput.setCustomValidity('価格は10円単位で入力してください');
  } else {
    priceInput.setCustomValidity('');  // バリデーションOK
  }
});

// validity プロパティで詳細を確認
const v = priceInput.validity;
v.valueMissing;    // required未入力
v.typeMismatch;    // type不一致
v.rangeUnderflow;  // min未満
v.rangeOverflow;   // max超過
v.customError;     // setCustomValidityでエラーあり

リアルタイムバリデーション

function setupValidation(input, validateFn) {
  const errorEl = document.createElement('span');
  errorEl.className = 'field-error';
  errorEl.style.color = '#e94560';
  errorEl.style.fontSize = '12px';
  input.after(errorEl);

  function validate() {
    const error = validateFn(input.value);
    errorEl.textContent = error || '';
    input.style.borderColor = error ? '#e94560' : '';
    return !error;
  }

  input.addEventListener('blur', validate);
  input.addEventListener('input', () => {
    if (errorEl.textContent) validate();  // エラー中は即時再検証
  });

  return validate;
}

// 使い方
const validateQty = setupValidation(
  document.getElementById('quantity'),
  (val) => {
    if (!val) return '数量を入力してください';
    if (Number(val) < 1) return '1以上を入力してください';
    if (Number(val) > 99) return '99以下で入力してください';
    return null;
  }
);

フォーム全体の検証

const form = document.getElementById('paymentForm');

form.addEventListener('submit', (e) => {
  e.preventDefault();

  // 全フィールドの標準バリデーション
  if (!form.checkValidity()) {
    form.reportValidity();
    return;
  }

  // カスタムクロスフィールド検証
  const paid = Number(form.paidAmount.value);
  const total = Number(form.totalAmount.value);

  if (paid < total) {
    form.paidAmount.setCustomValidity('支払額が合計未満です');
    form.reportValidity();
    form.paidAmount.setCustomValidity('');  // 次回のためにリセット
    return;
  }

  // お釣り計算して送信
  const change = paid - total;
  console.log(`お釣り: ¥${change.toLocaleString()}`);
  submitOrder(form);
});

サニタイズ(XSS防止の入力処理)

// HTMLエスケープ
function escapeHTML(str) {
  const div = document.createElement('div');
  div.appendChild(document.createTextNode(str));
  return div.innerHTML;
}

// 数値のみ許可する入力制限
function numericOnly(input) {
  input.addEventListener('input', () => {
    input.value = input.value.replace(/[^0-9]/g, '');
  });
}

// 長さ制限付きサニタイズ
function sanitizeInput(value, maxLength = 100) {
  return escapeHTML(value.trim().slice(0, maxLength));
}

// 使い方(商品名を安全に表示)
const rawName = '<script>alert("XSS")</script>弁当';
cell.textContent = rawName;          // ✅ textContent は安全
cell.innerHTML = escapeHTML(rawName); // ✅ エスケープ済み
cell.innerHTML = rawName;            // ❌ XSS脆弱性!

13. 状態管理パターン 中級

シンプルStore(状態の一元管理)

function createStore(initialState) {
  let state = { ...initialState };
  const listeners = new Set();

  return {
    getState() {
      return { ...state };
    },
    setState(updater) {
      const prev = state;
      state = { ...state, ...(typeof updater === 'function' ? updater(state) : updater) };
      listeners.forEach(fn => fn(state, prev));
    },
    subscribe(fn) {
      listeners.add(fn);
      return () => listeners.delete(fn);  // unsubscribe関数を返す
    },
  };
}

// 使い方
const store = createStore({ items: [], total: 0 });

// 変更を購読
const unsub = store.subscribe((newState, oldState) => {
  console.log('状態変更:', oldState, '→', newState);
  renderCart(newState);
});

// 状態を更新
store.setState(s => ({
  items: [...s.items, { name: 'コーヒー', price: 350 }],
  total: s.total + 350,
}));

Pub/Sub(EventEmitter)パターン

class EventEmitter {
  #handlers = {};

  on(event, handler) {
    (this.#handlers[event] ??= []).push(handler);
    return this;  // チェーン可能
  }

  off(event, handler) {
    const list = this.#handlers[event];
    if (list) this.#handlers[event] = list.filter(h => h !== handler);
    return this;
  }

  emit(event, ...args) {
    (this.#handlers[event] || []).forEach(h => h(...args));
  }
}

// POS向け使用例
const bus = new EventEmitter();

// 表示コンポーネントが購読
bus.on('cart:updated', (items) => renderTable(items));
bus.on('cart:updated', (items) => updateTotal(items));

// カートに追加する処理が発火
function addToCart(product) {
  cart.push(product);
  bus.emit('cart:updated', cart);
}

ステートマシン(注文フロー管理)

function createMachine(config) {
  let current = config.initial;

  return {
    get state() { return current; },

    send(event) {
      const transition = config.states[current]?.[event];
      if (!transition) {
        console.warn(`無効な遷移: ${current} → ${event}`);
        return current;
      }
      const next = typeof transition === 'string' ? transition : transition.target;
      if (transition.action) transition.action();
      current = next;
      return current;
    },
  };
}

// POS注文フロー
const orderFlow = createMachine({
  initial: 'idle',
  states: {
    idle:      { START_SCAN: 'scanning' },
    scanning:  { ADD_ITEM: 'scanning', REVIEW: 'reviewing' },
    reviewing: { PAY: 'paying', ADD_MORE: 'scanning' },
    paying:    {
      SUCCESS: { target: 'complete', action: () => printReceipt() },
      FAIL:    'reviewing',
    },
    complete:  { RESET: 'idle' },
  },
});

orderFlow.send('START_SCAN');  // → 'scanning'
orderFlow.send('REVIEW');      // → 'reviewing'
orderFlow.send('PAY');         // → 'paying'
orderFlow.send('SUCCESS');     // → 'complete'(レシート印刷)

Undo/Redo(操作の取り消し・やり直し)

function createHistory(initialState) {
  const past = [];
  const future = [];
  let present = initialState;

  return {
    get state() { return present; },
    get canUndo() { return past.length > 0; },
    get canRedo() { return future.length > 0; },

    push(newState) {
      past.push(present);
      present = newState;
      future.length = 0;  // 新操作時にredoをクリア
    },

    undo() {
      if (!this.canUndo) return present;
      future.push(present);
      present = past.pop();
      return present;
    },

    redo() {
      if (!this.canRedo) return present;
      past.push(present);
      present = future.pop();
      return present;
    },
  };
}

// 使い方
const cartHistory = createHistory([]);
cartHistory.push([{ name: 'コーヒー', qty: 1 }]);
cartHistory.push([{ name: 'コーヒー', qty: 1 }, { name: 'サンド', qty: 2 }]);
cartHistory.undo();  // コーヒーだけの状態に戻る
cartHistory.redo();  // サンドも追加された状態に戻る

14. セキュリティとXSS対策 中級

textContent vs innerHTML(XSS防止の基本)

const userInput = '<img src=x onerror=alert("XSS")>';
const el = document.getElementById('output');

// ✅ 安全 — テキストとして表示、HTMLとして解釈されない
el.textContent = userInput;

// ❌ 危険 — HTMLとして解釈され、スクリプトが実行される
el.innerHTML = userInput;

// ✅ 安全にHTMLを構築する方法
const li = document.createElement('li');
li.textContent = userInput;  // テキストのみ設定
list.appendChild(li);

HTMLエスケープユーティリティ

const ESCAPE_MAP = {
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&quot;',
  "'": '&#39;',
};

function escapeHTML(str) {
  return str.replace(/[&<>"']/g, (ch) => ESCAPE_MAP[ch]);
}

// innerHTML に挿入する必要がある場合に使用
const productName = '<script>悪意あるコード</script>';
row.innerHTML = `<td>${escapeHTML(productName)}</td>`;

安全なJSON解析とeval禁止

// ❌ eval は絶対に使わない(任意コード実行のリスク)
const data = eval('(' + jsonString + ')');  // 危険!

// ✅ JSON.parse を使う
try {
  const data = JSON.parse(jsonString);
} catch (e) {
  console.error('不正なJSONデータ:', e.message);
}

// ✅ バーコードデータも安全に処理
function parseBarcode(raw) {
  const cleaned = raw.replace(/[^0-9]/g, '');  // 数字以外を除去
  if (cleaned.length !== 13) return null;
  return cleaned;
}

機密データの安全な取り扱い

// 決済後にカード情報をDOMとメモリから消去
function clearSensitiveData() {
  // DOM上の値をクリア
  const cardInput = document.getElementById('cardNumber');
  if (cardInput) {
    cardInput.value = '';
    cardInput.setAttribute('autocomplete', 'off');
  }

  // マスキング表示(下4桁のみ)
  const display = document.getElementById('cardDisplay');
  if (display) display.textContent = '**** **** **** 1234';
}

// カード番号のマスキング関数
function maskCardNumber(num) {
  const last4 = num.slice(-4);
  return `**** **** **** ${last4}`;
}

// localStorageに機密情報を保存しない
// ❌ localStorage.setItem('cardNumber', '4111111111111111');
// ✅ サーバー側で安全に管理し、トークンのみ保持
localStorage.setItem('paymentToken', 'tok_abc123');

15. パフォーマンス最適化 中級

スロットル(高頻度イベントの間引き)

function throttle(fn, interval) {
  let lastTime = 0;
  let timer = null;

  return function(...args) {
    const now = Date.now();

    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    } else {
      clearTimeout(timer);
      timer = setTimeout(() => {
        lastTime = Date.now();
        fn.apply(this, args);
      }, interval - (now - lastTime));
    }
  };
}

// デバウンスとの違い:
// debounce → 入力が止まってから実行(検索向き)
// throttle → 一定間隔で実行(スクロール・リサイズ向き)

// 使い方: バーコードスキャナの連続入力を間引く
const handleScan = throttle((code) => {
  lookupProduct(code);
}, 300);

DocumentFragment(一括DOM更新)

// ❌ 遅い — ループ内で毎回DOMに追加(リフローが毎回発生)
products.forEach(p => {
  const li = document.createElement('li');
  li.textContent = `${p.name} ¥${p.price}`;
  list.appendChild(li);  // 毎回DOM操作が発生
});

// ✅ 速い — DocumentFragmentにまとめて一度だけ追加
const fragment = document.createDocumentFragment();
products.forEach(p => {
  const li = document.createElement('li');
  li.textContent = `${p.name} ¥${p.price}`;
  fragment.appendChild(li);  // メモリ上で構築
});
list.appendChild(fragment);  // 1回だけDOM操作

requestAnimationFrame(スムーズなUI更新)

// 合計金額のカウントアップアニメーション
function animateCounter(element, from, to, duration = 500) {
  const start = performance.now();

  function update(currentTime) {
    const elapsed = currentTime - start;
    const progress = Math.min(elapsed / duration, 1);

    // イージング(ease-out)
    const eased = 1 - Math.pow(1 - progress, 3);
    const current = Math.floor(from + (to - from) * eased);

    element.textContent = `¥${current.toLocaleString()}`;

    if (progress < 1) {
      requestAnimationFrame(update);
    }
  }

  requestAnimationFrame(update);
}

// 使い方
const totalEl = document.getElementById('total');
animateCounter(totalEl, 0, 12500);  // ¥0 → ¥12,500

遅延読み込み(Lazy Loading)

// ✅ HTML属性だけで画像の遅延読み込み(最もシンプル)
// <img src="product.jpg" loading="lazy" alt="商品画像">

// IntersectionObserverで自前実装(より細かい制御)
function lazyLoad(selector = 'img[data-src]') {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        img.removeAttribute('data-src');
        observer.unobserve(img);
      }
    });
  }, { rootMargin: '200px' });  // 200px手前で読み込み開始

  document.querySelectorAll(selector).forEach(img => {
    observer.observe(img);
  });
}

// ページ読み込み時に実行
lazyLoad();

16. 高度なDOM操作 中級

カスタムイベント(コンポーネント間通信)

// カスタムイベントを発火
function addToCart(product) {
  cart.push(product);

  // detail にデータを乗せてイベント発火
  document.dispatchEvent(new CustomEvent('cart:updated', {
    detail: {
      items: [...cart],
      total: cart.reduce((s, i) => s + i.price * i.qty, 0),
    },
  }));
}

// 別の場所でリッスン
document.addEventListener('cart:updated', (e) => {
  const { items, total } = e.detail;
  document.getElementById('itemCount').textContent = items.length;
  document.getElementById('total').textContent = `¥${total.toLocaleString()}`;
});

// 特定要素でバブルさせたい場合
element.dispatchEvent(new CustomEvent('item:selected', {
  detail: { id: 42 },
  bubbles: true,  // 親要素にも伝搬
}));

MutationObserver(DOM変更の監視)

// テーブルの行数変化を監視
const tbody = document.querySelector('#cartTable tbody');

const observer = new MutationObserver((mutations) => {
  mutations.forEach(m => {
    if (m.type === 'childList') {
      const rowCount = tbody.rows.length;
      document.getElementById('itemCount').textContent = rowCount;
      console.log(`行数変化: ${rowCount}行`);
    }
  });
});

// 監視を開始
observer.observe(tbody, {
  childList: true,   // 子要素の追加・削除
  subtree: false,    // 孫要素は監視しない
});

// 監視を停止
// observer.disconnect();

IntersectionObserver(無限スクロール)

// 商品カタログの無限スクロール
let page = 1;
let loading = false;

const sentinel = document.getElementById('loadMore');

const observer = new IntersectionObserver(async (entries) => {
  const entry = entries[0];
  if (!entry.isIntersecting || loading) return;

  loading = true;
  sentinel.textContent = '読み込み中...';

  try {
    const res = await fetch(`/api/products?page=${page}`);
    const products = await res.json();

    if (products.length === 0) {
      observer.disconnect();
      sentinel.textContent = '全商品を表示しました';
      return;
    }

    renderProducts(products);
    page++;
  } finally {
    loading = false;
    sentinel.textContent = '';
  }
}, { rootMargin: '300px' });

observer.observe(sentinel);