1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

モダンjsをしつこく覚える覚書

Last updated at Posted at 2025-09-22

JavaScript基礎を“実務仕様”に:アロー関数/分割代入+デフォルト値/配列・fetch・DOM・XSS(詳解版)

目標:落ちない・読める・拡張しやすい
コアは アロー関数分割代入+デフォルト値(安全ガード)
配列処理・fetchの堅牢化・DOM・XSSまで、最小サンプル+厚い解説で一気に。


目次

  1. アロー関数(Arrow Function)

  2. 分割代入+デフォルト値(安全ガード・超重要)

  3. テンプレート文字列(安全に使う小技)

  4. 配列メソッド(重複排除・集計・条件連結・ページング)

  5. fetchの安全テンプレ(okチェック/タイムアウト/再試行)

  6. DOM(querySelector / イベント / 委譲 / template)

  7. XSS対策(まずtextContent、innerHTMLは最後の手段)

  8. 仕上げテンプレ&ドリル


アロー関数(Arrow Function)

// 最短:式が1つなら {} と return を省略できる
const twice = (a) => a * 2;
console.log(twice(3)); // 6

// ブロックにしたら return が必要
const sumTimes2 = (a, b) => {
  const s = a + b;
  return s * 2;
};

なぜ使う?

  • 可読性:短いコールバックに最適(map, filter, reduce)。
  • 束縛の安全性constで意図しない上書き事故を防止。
  • 記法の一貫性:関数式として変数に束ねると、依存関係の見通しが良くなる。

thisの挙動(最重要ポイント)

  • アロー関数は自分のthisを持たず外側のthisをキャプチャする。
    → クラス/オブジェクトのメソッド内コールバックに相性◎
    → DOMイベントで**「this=要素」が欲しいときは通常function**が安全。
// ◯ 外側thisを引き継ぐ:setIntervalのコールバックに最適
const counter = {
  n: 0,
  start() {
    this.timer = setInterval(() => { this.n++; console.log(this.n); }, 500);
  },
  stop(){ clearInterval(this.timer); }
};
counter.start();
setTimeout(() => counter.stop(), 1600);

// △ 要素をthisにしたい:通常functionで
document.body.addEventListener('click', function () {
  console.log('clicked:', this.tagName);
});

設計小話
“ワンライナー崇拝”は禁物。「意図ごとに行を分ける」=未来の自分へのギフトです。


分割代入+デフォルト値(安全ガード・超重要)

ここが“落ちないコード”の核心。未指定・部分指定・順不同でも例外にしない受け口を作る。

外側と内側デフォルトの役割

// 丸暗記OK:受け口の鉄板
function f({ a = [], b = 0, c = '' } = {}) {
  console.log(a, b, c);
}
  • 外側 = {}f() のような未指定呼び出しでも {} に置き換わる → エラーしない。
  • 内側 a=[]:キー未指定でも初期型が入る → 後段の map/length/数値計算 が安定。
  • 順不同:オブジェクト引数なので、追加・削除・順序変更に強い。

呼び方バリエーションと失敗例

f();                 // [] 0 ''
f({});               // [] 0 ''
f({ a:[1,2] });      // [1,2] 0 ''
f({ b:42, c:'X' });  // [] 42 'X'

// ❌ 外側デフォルトなしは未指定で落ちる
function bad({ a = [] }) {}
// bad(); // TypeError: Cannot destructure property 'a' of 'undefined'

解説の深掘り

  • 外側」は“引数そのもの”に対する保険。
  • 内側」は“各キー”に対する保険。
  • どちらかが欠けると、未指定または個別未定義で落ちる。両方必要

実務パターン①:必須と任意を分ける

function createUser(required, opts = {}) {
  const { id, name } = required; // 必須は別引数で明示
  const { role = 'guest', email = '', flags = [] } = opts; // 任意はデフォルト付き
  return { id, name, role, email, flags };
}

console.log(createUser({ id:1, name:'Keiko' }, { role:'admin' }));

なぜ良い?

  • 呼び出し側が“必須を渡していない”とビルド/レビューで気づきやすい
  • 任意は初期値で安全後から項目追加しやすい

実務パターン②:ネスト/リネーム/正規化

function setup({
  ui:  { theme = 'light', lang = 'ja' } = {},
  api: { base: API = '/api' } = {},     // api.base を API 名で受ける(リネーム)
} = {}) {
  console.log(theme, lang, API);
}

setup({ ui:{theme:'dark'}, api:{base:'/v1'} }); // dark ja /v1
setup({ ui:{lang:'en'} });                      // light en /api
setup();                                        // light ja /api

リネームの効能

  • API の用途が明確になり、後工程の命名衝突も防げる。
  • base という変数は作られない点に注意(束縛されるのは API)。

正規化して返す(どこでも同じ形が得られる)

function normalizeConfig({
  ui:  { theme = 'light', lang = 'ja' } = {},
  api: { base: API = '/api' } = {},
} = {}) {
  return { ui: { theme, lang }, api: { base: API } };
}

常に欠けが補われた設定が返るので、呼び出し元が条件分岐を減らせる=バグ源が減る。

実務パターン③:残余プロパティ&型の正規化

function normalize({ a = [], b = 0, c = '', ...rest } = {}) {
  if (!Array.isArray(a)) a = [a];     // aは配列に正規化(単体でもOK)
  b = Number.isFinite(b) ? b : 0;     // bは数値に
  c = String(c);                      // cは文字列に
  return { a, b, c, options: rest };  // 未知キーは options に逃がす
}

なぜ現場で効く?

  • 外部入力旧コードから来る“型ブレ”を受け口で吸収できる。
  • 本体ロジックを型前提でシンプルに書ける(防御は入口で)。

TypeScriptで型を添えると何が嬉しいか

type Options = {
  ui?:  { theme?: 'light'|'dark', lang?: 'ja'|'en' },
  api?: { base?: string },
};

function setup({ ui: { theme = 'light', lang = 'ja' } = {},
                 api: { base: API = '/api' } = {} }: Options = {}) {
  console.log(theme, lang, API);
}
  • ?任意を明示 → 呼び出し側の補完・Lintが効く
  • パラメータの意図(上書き可能・省略可能)が型に表れる

暗記ポイントまとめ

  • 外側 = {}:未指定でも落ちない
  • 内側デフォルト:型初期値で後工程を安定化
  • リネームで意味明確&衝突回避
  • 正規化して返す:いつでも同じ形に整える

テンプレート文字列(安全に使う小技)

// 動的部分だけをエスケープするタグ関数
const esc = (s, ...v) => s.reduce((a, x, i) => {
  const val = v[i] == null ? '' : String(v[i])
    .replaceAll('&','&amp;').replaceAll('<','&lt;')
    .replaceAll('>','&gt;').replaceAll('"','&quot;')
    .replaceAll("'",'&#39;');
  return a + x + (i < v.length ? val : '');
}, '');

const name = '<img src=x onerror=alert(1)>';
document.body.innerHTML = esc`<p>こんにちは、${name}さん</p>`;

補足:URLは encodeURIComponent / URLSearchParams を使うとバグが激減

const q = '大阪 看護師';
const url = `/search?${new URLSearchParams({ q, limit: 20 })}`;

配列メソッド(重複排除・集計・条件連結・ページング)

const jobs = [
  { id:1, pref:'大阪', emp:'正社員' },
  { id:2, pref:'大阪', emp:'派遣' },
  { id:3, pref:'奈良', emp:'正社員' },
];

// 重複なし(map → Set → スプレッド)
const uniquePrefs = [...new Set(jobs.map(j => j.pref))]; // ["大阪","奈良"]

// 集計(reduce 基本形)
const countsByEmp = jobs.reduce((acc, j) => {
  acc[j.emp] = (acc[j.emp] ?? 0) + 1;
  return acc;
}, {}); // { 正社員:2, 派遣:1 }

// 二次元集計(pref × emp)
const byPrefAndEmp = jobs.reduce((acc, j) => {
  acc[j.pref] ??= {};
  acc[j.pref][j.emp] = (acc[j.pref][j.emp] ?? 0) + 1;
  return acc;
}, {});

なぜこの形?

  • 重複排除は“プリミティブ”にしてからSetに入れるのが最短。
  • 集計は「acc[key] = (acc[key] ?? 0) + 1」を手に覚えさせる
  • reduce初期値 {} を必ず渡す(渡さないと最初の要素が初期値になって破綻)。

条件文字列の組み立て(空は除外)

const conditions = [
  ['pref', [27,28]],
  ['job',  [3]],
  ['emp',  []],
].filter(([, ids]) => ids?.length)
 .map(([k, ids]) => `(field:${k}_ids:(${ids.join(' OR ')}))`)
 .join(' AND ');

ページング(非破壊)

const items = Array.from({ length: 23 }, (_, i) => i + 1);
const PAGE = 2, SIZE = 5; // 0始まり
const pageItems  = items.slice(PAGE * SIZE, PAGE * SIZE + SIZE); // [11..15]
const totalPages = Math.ceil(items.length / SIZE);               // 5

fetchの安全テンプレ(okチェック/タイムアウト/再試行)

fetch は 4xx/5xx でも例外を投げないres.ok が命

const getJson = async (url) => {
  try {
    const r = await fetch(url, { headers: { Accept:'application/json' } });
    if (!r.ok) throw new Error(`HTTP ${r.status}`); // ←ここで例外化
    return await r.json();
  } catch (e) {
    console.error('通信/JSON失敗:', e);
    return null; // 呼び出し側は null 判定で分岐
  }
};

const postJson = async (url, payload) => {
  try {
    const r = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type':'application/json', Accept:'application/json' },
      body: JSON.stringify(payload),
    });
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    return await r.json();
  } catch (e) {
    console.error(e);
    return null;
  }
};

タイムアウト(回線詰まり対策)+指数バックオフ再試行

const fetchWithTimeout = (url, opts = {}, timeoutMs = 8000) => {
  const ac = new AbortController();
  const id = setTimeout(() => ac.abort(), timeoutMs);
  return fetch(url, { ...opts, signal: ac.signal })
    .finally(() => clearTimeout(id));
};

const retry = async (fn, tries = 3, base = 300) => {
  let err;
  for (let i = 0; i < tries; i++) {
    try { return await fn(); }
    catch (e) { err = e; await new Promise(r => setTimeout(r, base * 2**i)); }
  }
  throw err;
};

設計メモ

  • UI側で null明示的に扱う設計にすると“謎の未定義挙動”を撲滅できる。
  • “どこで例外にするか”を受け口で統一するとデバッグが楽。

DOM(querySelector / イベント / 委譲 / template)

<button id="add">追加</button>
<ul id="list"></ul>
<script>
  const add  = document.querySelector('#add');
  const list = document.querySelector('#list');

  add.addEventListener('click', () => {
    const li = document.createElement('li');
    li.textContent = '項目';
    list.appendChild(li);
  });

  // 委譲:新規追加にも効く
  list.addEventListener('click', (e) => {
    const li = e.target.closest('li');
    if (!li || !list.contains(li)) return;
    console.log('clicked:', li.textContent);
  });
</script>

templateでレンダリングの型を固定化

<template id="job-tpl">
  <article class="job">
    <h2 class="title"></h2>
    <p class="meta"></p>
  </article>
</template>
<div id="results"></div>
<script>
  const tpl = document.querySelector('#job-tpl');
  const results = document.querySelector('#results');

  function renderJob(job){
    const node = tpl.content.cloneNode(true);
    node.querySelector('.title').textContent = job.title;         // ← textContentで安全
    node.querySelector('.meta').textContent  = `${job.date} / ${job.facility}`;
    results.appendChild(node);
  }
</script>

XSS対策(まずtextContent、innerHTMLは最後の手段)

// 最優先:textContent で文字として差し込む
const p = document.createElement('p');
p.textContent = userInput; // <script> 等も“文字”になる
out.appendChild(p);

// innerHTML を使う場合:変数部分はエスケープしてから
const escapeHtml = (s) => String(s ?? '')
  .replaceAll('&','&amp;').replaceAll('<','&lt;')
  .replaceAll('>','&gt;').replaceAll('"','&quot;')
  .replaceAll("'",'&#39;');

container.innerHTML = `<p>${escapeHtml(userInput)}</p>`;

補足:ライブラリでも、HTMLを挿入するAPIは慎重に。
まずは textContent、次に要素を組み立てる、最終手段としてinnerHTML


仕上げテンプレ&ドリル

最小テンプレ(コピペ用)

// 受け口(安全ガード)
function args({ a = [], b = 0, c = '' } = {}) { /*...*/ }

// 配列パイプライン(抽出→変換→切り出し)
const result = items.filter(isValid).map(toVM).slice(0, 20);

// 空を除外してクエリ生成
const qs = Object.entries({ q:'大阪', limit:20, emp:'' })
  .filter(([,v]) => v !== '' && v != null)
  .reduce((p,[k,v]) => (p.append(k, v), p), new URLSearchParams());
const url = `/search?${qs}`;

ミニドリル

  1. 配列[{city:'大阪', emp:'正社員'}, ...] → ①ユニークcity ②emp件数 ③city×emp二次元集計
  2. オプション引数search({ q='', pref=[], emp=[], limit=20 }={}) を“落ちない受け口”で
  3. fetchgetJson('/api/jobs')!dataなら再試行ボタン・HTTPコード表示
  4. DOM:「追加」で<li>追加、「削除」で末尾<li>削除(イベントはJS側)
  5. XSS<img src=x onerror=...> を入力し、textContentinnerHTMLの差を確認(ローカルで)

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?