POS開発 フロントエンド チートコード
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でもない)
truthy と falsy
// ── falsy な値(if文でfalseとして扱われる)── // false, 0, ""(空文字), null, undefined, NaN の6つだけ if (!false) { } // true if (!0) { } // true if (!"") { } // true if (!null) { } // true if (!undefined) { } // true if (!NaN) { } // true // ── truthy な値(上記以外はすべてtrue)── if ("0") { } // true(文字列の"0"はtruthy!) if ([]) { } // true(空配列もtruthy!) if ({}) { } // true(空オブジェクトもtruthy!) // ── POS実用例 ── // 在庫があるかチェック(0はfalsy) const stock = 0; if (stock) { console.log("在庫あり"); // ← stock=0 だと実行されない } // 数値の0を正しく判定するなら明示的に比較する if (stock !== undefined && stock !== null) { console.log(`在庫: ${stock}個`); // stock=0 でも実行される } // 入力値の空チェック(空文字はfalsy) const input = document.getElementById('barcode').value; if (!input) { console.log("バーコードを入力してください"); }
条件分岐
// 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 }; }; // 関数式(変数に関数を代入、巻き上げされない) const calcDiscount = function(price, rate) { return Math.floor(price * rate); }; // 即時実行関数 (IIFE) (() => { const secret = "外からアクセスできない"; })();
関数スコープ
// var は「関数スコープ」(関数の中でだけ閉じる) function example() { if (true) { var x = 10; // if の外でもアクセスできてしまう } console.log(x); // 10 ← ブロックの外に漏れる! } // let / const は「ブロックスコープ」({} の中で閉じる) function example2() { if (true) { let y = 10; // if の中だけ有効 const z = 20; // if の中だけ有効 } // console.log(y); // ReferenceError(アクセス不可) } // ← だから var は使わず、const / let を使う
コールバック関数
// コールバック = 関数を「引数として」別の関数に渡すパターン // 基本形: 処理完了後に呼び出す関数を渡す function processOrder(order, callback) { const total = order.price * order.qty; callback(total); // 処理後にコールバックを実行 } processOrder({ price: 500, qty: 3 }, (total) => { console.log(`合計: ${total}円`); // "合計: 1500円" }); // よく使われるコールバックの例 // 1. イベントリスナー(第2引数がコールバック) btn.addEventListener('click', () => { /* この関数がコールバック */ }); // 2. forEach(引数の関数がコールバック) items.forEach((item) => { /* この関数がコールバック */ }); // 3. setTimeout(第1引数がコールバック) setTimeout(() => { console.log("3秒後に実行"); }, 3000);
テンプレートリテラル
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] // 要素のスキップ(不要な位置はカンマだけ書く) const [, , third] = ["a", "b", "c"]; // third="c" // デフォルト値 const [x = 0, y = 0] = [42]; // x=42, y=0 // 関数の戻り値を分割代入(よく使うパターン) function parsePrice(input) { return [parseInt(input), input.includes("税込")]; } const [amount, taxIncluded] = parsePrice("350円(税込)"); // 値のスワップ let a = 1, b = 2; [a, b] = [b, a]; // a=2, b=1 // ── 分割代入の実用例 ── // 関数パラメータで直接分割代入 function printReceipt({ name, price, qty = 1 }) { console.log(`${name} x${qty} = ¥${price * qty}`); } printReceipt({ name: "コーヒー", price: 350 }); // APIレスポンスから必要な部分だけ取得 const { data: { items, total }, status } = await fetch(url).then(r => r.json()); // ── スプレッド構文(配列) ── const newItems = [...items, "新商品"]; // 末尾に追加 const prepend = ["先頭商品", ...items]; // 先頭に追加 const copy = [...items]; // 浅いコピー(元を変えずに操作したい時) const concat = [...arr1, ...arr2]; // 結合 // 特定要素を除外して新しい配列を作る(イミュータブル削除) const removed = items.filter((_, i) => i !== 2); // index2を除外 // 特定要素を置換して新しい配列を作る(イミュータブル更新) const replaced = items.map((item, i) => i === 2 ? { ...item, price: 400 } : item ); // ── スプレッド構文(オブジェクト) ── const updated = { ...product, price: 400 }; // 一部上書き const merged = { ...defaults, ...userConfig }; // マージ(後の値が優先) // ── レスト構文(...を左辺で使う = 残りを集める) ── const { id, ...others } = { id: 1, name: "A", price: 100 }; // id=1, others={ name:"A", price:100 }(特定キーを除外したい時に便利) // 関数の可変長引数 function sum(...nums) { return nums.reduce((a, b) => a + b, 0); } sum(1, 2, 3); // 6 // 関数の引数に展開 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日"
文字列の検索で条件分岐
// 商品名に特定の文字列が含まれるか判定 const productName = "有機野菜サラダ"; if (productName.includes("有機")) { console.log("オーガニック商品ラベルを表示"); } // 検索キーワードで商品をフィルタリング const keyword = searchInput.value; const filtered = products.filter(p => p.name.includes(keyword) ); // 大文字・小文字を区別しない検索 const query = "coffee"; const found = products.filter(p => p.name.toLowerCase().includes(query.toLowerCase()) ); // バーコードの先頭で分類判定 const barcode = "4901234567890"; if (barcode.startsWith("49")) { console.log("日本の商品(GS1 Japan)"); } // 複数キーワードのいずれかを含むか const note = "冷蔵・要冷蔵"; const coldKeywords = ["冷蔵", "冷凍", "チルド"]; if (coldKeywords.some(kw => note.includes(kw))) { console.log("要冷蔵商品"); }
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; // プロパティとして直接設定も可
フォーム要素の値取得(チェック・ラジオ・セレクト)
// ── チェックボックス ── // HTML: <input type="checkbox" id="taxExempt"> 免税対象 const taxExempt = document.getElementById('taxExempt'); taxExempt.checked; // true or false(ON/OFFの状態) taxExempt.checked = true; // プログラムでONにする // 条件分岐で使う if (taxExempt.checked) { console.log("免税処理を適用"); } // 変更を検知 taxExempt.addEventListener('change', (e) => { console.log(e.target.checked); // true / false }); // 複数チェックボックスの選択値をまとめて取得 // HTML: <input type="checkbox" name="options" value="袋"> 袋 // <input type="checkbox" name="options" value="箸"> 箸 const checked = [...document.querySelectorAll('input[name="options"]:checked')] .map(el => el.value); // 例: ["袋"](チェックされたものだけ配列になる) // ── ラジオボタン ── // HTML: <input type="radio" name="payment" value="cash"> 現金 // <input type="radio" name="payment" value="card"> カード // <input type="radio" name="payment" value="qr"> QR決済 const selected = document.querySelector('input[name="payment"]:checked'); const paymentMethod = selected ? selected.value : null; // "cash" / "card" / "qr" / null(未選択時) // 変更を検知(name属性でグルーピング) document.querySelectorAll('input[name="payment"]').forEach(radio => { radio.addEventListener('change', (e) => { console.log(`決済方法: ${e.target.value}`); }); }); // ── セレクトボックス ── // HTML: <select id="category"> // <option value="">カテゴリを選択</option> // <option value="food">食品</option> // <option value="drink">飲料</option> // </select> const select = document.getElementById('category'); select.value; // 選択中のoptionのvalue(例: "food") select.selectedIndex; // 選択中のインデックス(0始まり) select.options; // 全optionのHTMLCollection select.options[select.selectedIndex].text; // 表示テキスト(例: "食品") // プログラムで選択を変更 select.value = "drink"; // 変更を検知 select.addEventListener('change', (e) => { const text = e.target.options[e.target.selectedIndex].text; console.log(`${e.target.value}: ${text}`); // "food: 食品" });
イベントリスナー
// クリック 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: 指定位置で削除/挿入(元の配列を変更する=破壊的) ── // splice(開始index, 削除する個数, ...挿入する要素) const cart = ["コーヒー", "サンド", "紅茶", "ケーキ"]; cart.splice(1, 1); // cart → ["コーヒー", "紅茶", "ケーキ"] ※削除された ["サンド"] を返す cart.splice(1, 0, "ジュース"); // cart → ["コーヒー", "ジュース", "紅茶", "ケーキ"] ※削除0個=挿入のみ cart.splice(2, 1, "抹茶", "水"); // cart → ["コーヒー", "ジュース", "抹茶", "水", "ケーキ"] ※削除+挿入=置換 cart.splice(-1, 1); // cart → ["コーヒー", "ジュース", "抹茶", "水"] ※負のindexは末尾から // ── slice: 一部を切り出して新しい配列を返す(非破壊) ── // slice(開始index, 終了index) ※終了indexの要素は含まない const items = ["A", "B", "C", "D", "E"]; items.slice(1, 3); // ["B", "C"] index1〜2 items.slice(2); // ["C", "D", "E"] index2から最後まで items.slice(-2); // ["D", "E"] 末尾から2つ items.slice(); // ["A","B","C","D","E"] 全体のコピー // POS実用例: ページネーション const page = 2, perPage = 10; const pageItems = allItems.slice((page - 1) * perPage, page * perPage); // ── splice vs slice 早見表 ── // splice → 元の配列を変更する(破壊的)、削除した要素を返す // slice → 元の配列を変更しない(非破壊)、切り出した新配列を返す // ── 元の配列を変更しない操作(イミュータブル) ── 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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }; 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);