関数分割とは何か
関数分割は「大きな処理を、明確な役割ごとの“小さな関数”に切り出して組み合わせる」設計です。ここが重要です:1つの関数は1つの責務(Single Responsibility)。入力と出力をはっきり決め、内部状態に依存しない“純粋な関数”を増やすと、読みやすさ・テスト容易性・性能最適化が同時に手に入ります。
// 大きな一体関数(読みにくい)
function process(items, cfg) {
const normalized = items.map(x => ({ ...x, name: x.name.trim().toLowerCase() }));
const filtered = normalized.filter(x => !cfg.enabledOnly || x.active);
const sorted = filtered.toSorted((a,b)=>a.price-b.price);
return sorted.slice((cfg.page-1)*cfg.perPage, cfg.page*cfg.perPage);
}
// 分割版(意図が伝わる)
const normalize = x => ({ ...x, name: x.name.trim().toLowerCase() });
const filterBy = (x, cfg) => (!cfg.enabledOnly || x.active);
const sortBy = (a, b) => a.price - b.price;
const paginate = (list, page, per) => list.slice((page-1)*per, (page-1)*per + per);
function processFast(items, cfg) {
const normalized = items.map(normalize);
const filtered = normalized.filter(x => filterBy(x, cfg));
const sorted = filtered.toSorted(sortBy);
return paginate(sorted, cfg.page, cfg.perPage);
}
JavaScript分割の基本方針(責務、入出力、純粋性)
責務を1つに絞る
関数は「名前通りの1つの仕事」だけをします。フィルタはフィルタ、変換は変換、集計は集計。余計な副作用(ログ、状態書き換え)を混ぜないことで、再利用性が上がり、結合が緩くなります。
const normalizeName = s => s.trim().toLowerCase();
const toViewRow = x => ({ id: x.id, name: normalizeName(x.name), price: x.price });
JavaScript入出力の契約を明確に
引数の型と返り値の型を決め、欠損の扱い(null/undefined)や既定値を統一します。契約が明確だと、呼び出し側の防御コードが減り、パイプラインが真っ直ぐになります。
function toPrice(s, def = null) {
const n = Number.parseFloat(String(s));
return Number.isFinite(n) ? n : def; // 失敗時は def を返す契約
}
JavaScript純粋な関数を基本に
同じ入力は同じ出力、外部状態を書き換えない。純粋性が高いほどテストが容易になり、並列化やメモ化の恩恵を受けやすくなります。
// 純粋:入力をコピーして新値を返す
const setActive = x => ({ ...x, active: true });
JavaScript実務での分割例(配列・オブジェクトの定番処理)
フィルタ、変換、ソート、ページングの分離
表示用の典型処理は“段ごと関数”に分けると、差し替えやテストが容易です。フィルタ条件やソートキーを入れ替えるだけで使い回せます。
const whereEnabled = f => x => (!f.enabledOnly || x.active);
const whereQuery = f => {
const q = f.q?.trim().toLowerCase();
return !q ? () => true : x => x.name.toLowerCase().includes(q);
};
const cmpPriceAsc = (a, b) => a.price - b.price;
function querySortPage(items, f) {
const pred = x => whereEnabled(f)(x) && whereQuery(f)(x);
const filtered = items.filter(pred);
const sorted = filtered.toSorted(cmpPriceAsc);
return paginate(sorted, f.page ?? 1, f.perPage ?? 20);
}
JavaScriptgroupBy、辞書化、統計の分割
“形を作る”関数と“値を計算する”関数を分ける。構造とロジックが独立し、変更コストが下がります。
const groupBy = (list, keyFn) =>
list.reduce((acc, x) => ((acc[keyFn(x)] ??= []).push(x), acc), {});
const toDict = (list, key = "id") =>
Object.fromEntries(list.map(x => [String(x[key]), x]));
const statsPrice = list =>
list.reduce((a, x) => ({ count: a.count+1, sum: a.sum + (x.price ?? 0) }), { count: 0, sum: 0 });
JavaScript可読性と速度の両立(分割しつつ最適化)
宣言と実装の二層化
外からは分かりやすい“宣言的”関数名で並べ、内部では1パス融合や前計算で高速化します。意図を壊さずに最適化できるのが利点です。
function applyFilters(items, f) {
const out = [];
const q = f.q?.trim().toLowerCase();
for (const x of items) {
if (f.enabledOnly && !x.active) continue;
if (q && !x.name.toLowerCase().includes(q)) continue;
out.push(x);
}
return out;
}
function pipeline(items, f) {
const filtered = applyFilters(items, f); // 読みやすい名前
const sorted = filtered.toSorted(cmpPriceAsc);
return paginate(sorted, f.page, f.perPage);
}
JavaScript前計算キーで重い処理を外へ出す
正規化や日付変換を先に1回だけ行い、後段の関数は軽いキーを使って速く動かす。分割の恩恵を保ったまま性能が出ます。
const prepare = x => ({ x, nameKey: x.name.trim().toLowerCase(), t: new Date(x.createdAt).getTime() });
function filterPrepared(prepared, q, cutoff) {
const keyQ = q?.trim().toLowerCase();
return prepared.filter(p => (!keyQ || p.nameKey.includes(keyQ)) && p.t >= cutoff);
}
JavaScript分割の落とし穴と回避(重要ポイントの深掘り)
過剰分割で意図が見えなくなる
小さくしすぎると“つながり”が見えず、読みにくい。1関数が“意味のあるひとかたまり”になるよう、粒度を意識します。目安は10〜30行程度、引数は3つ以内に収めると扱いやすいことが多いです。
引数の乱立とクロージャ依存
引数が増えすぎると誤用が増えます。設定は“パラメータオブジェクト”でまとめるか、部分適用で閉じ込めると安全です。ただし大きなオブジェクトをクロージャにキャプチャしすぎるとメモリが膨らむため、必要最小限に留めます。
const makeWhere = f => x => (!f.enabledOnly || x.active) && (!f.q || x.name.includes(f.q));
JavaScript純粋性を壊す副作用
ログ、外部書き換え、破壊的操作が混ざると、再利用・テストが難しくなります。副作用は末端(入出力層)へ寄せ、ドメインロジックは純粋に保ちます。
テスト容易性とエラーハンドリング(分割の実務メリット)
小さなユニットテストで品質を担保
フィルタ条件、変換、比較関数などは入力と期待出力を固定しやすく、テストが極めて簡単です。大きな一体関数より、バグの局所化ができます。
// 例:比較関数のテスト
expect(cmpPriceAsc({price:1},{price:2})).toBeLessThan(0);
expect(cmpPriceAsc({price:2},{price:1})).toBeGreaterThan(0);
JavaScript失敗しやすい箇所を独立させる
数値変換、日付パース、正規化は失敗が起きやすい。変換関数を分離して、失敗時の既定値やエラーコードを統一すると、呼び出し側をシンプルに保てます。
function toDateMs(s) {
const t = new Date(s).getTime();
return Number.isNaN(t) ? null : t;
}
JavaScriptすぐ使える分割レシピ(現場の最短コード)
パイプを関数で表現
const normalize = x => ({ ...x, name: x.name.trim().toLowerCase() });
const where = f => x => (!f.enabledOnly || x.active) && (!f.q || x.name.toLowerCase().includes(f.q.toLowerCase()));
const sortBy = (a, b) => a.price - b.price;
function transform(items, f) {
return paginate(
items.map(normalize).filter(where(f)).toSorted(sortBy),
f.page ?? 1, f.perPage ?? 20
);
}
JavaScript1パス融合版(大規模向け)
function transformFast(items, f) {
const out = [];
const q = f.q?.trim().toLowerCase();
for (const it of items) {
const name = it.name.trim().toLowerCase();
if (f.enabledOnly && !it.active) continue;
if (q && !name.includes(q)) continue;
out.push({ id: it.id, name, price: it.price });
}
const sorted = out.toSorted((a,b)=>a.price-b.price);
return paginate(sorted, f.page ?? 1, f.perPage ?? 20);
}
JavaScriptまとめ
関数分割の核心は「1関数=1責務」「入出力の契約を明確に」「純粋性を保つ」です。フィルタ・変換・ソート・ページングを段ごとに分け、宣言的に並べると意図が伝わり、差し替えやテストが容易になります。性能が必要な箇所は内部で1パス融合や前計算キーを使い、可読性を崩さず高速化する“二層設計”を選ぶ。過剰分割や副作用混入を避け、失敗しやすい変換は独立させる。これを徹底すれば、初心者でも読みやすくて速い配列・オブジェクト処理を、安定した設計で組み立てられます。
