JavaScript基礎を“実務仕様”に:アロー関数/分割代入+デフォルト値/配列・fetch・DOM・XSS(詳解版)
目標:落ちない・読める・拡張しやすい。
コアは アロー関数 と 分割代入+デフォルト値(安全ガード)。
配列処理・fetchの堅牢化・DOM・XSSまで、最小サンプル+厚い解説で一気に。
目次
アロー関数(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('&','&').replaceAll('<','<')
.replaceAll('>','>').replaceAll('"','"')
.replaceAll("'",''');
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('&','&').replaceAll('<','<')
.replaceAll('>','>').replaceAll('"','"')
.replaceAll("'",''');
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}`;
ミニドリル
-
配列:
[{city:'大阪', emp:'正社員'}, ...]→ ①ユニークcity ②emp件数 ③city×emp二次元集計 -
オプション引数:
search({ q='', pref=[], emp=[], limit=20 }={})を“落ちない受け口”で -
fetch:
getJson('/api/jobs')、!dataなら再試行ボタン・HTTPコード表示 -
DOM:「追加」で
<li>追加、「削除」で末尾<li>削除(イベントはJS側) -
XSS:
<img src=x onerror=...>を入力し、textContentとinnerHTMLの差を確認(ローカルで)