再帰処理とは何か
再帰は「関数が自分自身を呼び出して、問題を“同じ形のより小さな問題”に分解して解く」テクニックです。ここが重要です:必ず“止まる条件(ベースケース)”を先に書き、次に“1歩進める処理(再帰ケース)”を書く。この2つが揃うと、ネストされた配列・オブジェクト・ツリー構造を短く分かりやすく処理できます。
// 例:配列の合計(再帰)
function sum(arr) {
if (arr.length === 0) return 0; // ベースケース
return arr[0] + sum(arr.slice(1)); // 再帰ケース
}
JavaScriptベースケースと再帰ケース(核心の設計)
ベースケース(止まる条件を先に書く)
再帰が無限に続かないよう、“これ以上分解しない最小単位”を返します。ネスト構造では「null/undefined」「プリミティブ(数値や文字列)」「空配列・空オブジェクト」をベースケースにするのが定番です。
function size(node) {
if (!node) return 0; // 欠損なら 0
if (!Array.isArray(node.children)) return 1; // 子が無ければ 1(自分)
// 再帰ケースへ
}
JavaScript再帰ケース(1歩だけ進めて自分を呼ぶ)
“今”の仕事を少しだけ進め、残りを同じ関数に任せます。配列なら先頭+残り、ツリーなら自分+各子に対して再帰を呼ぶ、という形が分かりやすいです。
function size(node) {
if (!node) return 0;
const children = node.children ?? [];
const childTotal = children.reduce((acc, ch) => acc + size(ch), 0);
return 1 + childTotal; // 自分 + 子の合計
}
JavaScriptツリー構造の走査(深さ優先探索の基本)
全ノードを訪問する(DFS)
深さ優先(DFS)は「自分を処理→子を順に処理」のシンプルな再帰です。ネストオブジェクト・フォルダ階層・コメントスレッドなどに使えます。
function walk(node, visit) {
if (!node) return;
visit(node); // ここで処理(表示・集計など)
for (const child of node.children ?? []) {
walk(child, visit);
}
}
// 使い方
const tree = { name: "root", children: [{ name: "A" }, { name: "B", children: [{ name: "B1" }] }] };
walk(tree, n => console.log(n.name)); // root, A, B, B1
JavaScript条件に一致するノードを探す
見つかったらそこで終わる早期終了パターン。再帰らしさと効率を両立できます。
function find(node, pred) {
if (!node) return null;
if (pred(node)) return node;
for (const child of node.children ?? []) {
const found = find(child, pred);
if (found) return found; // 見つかったら返す
}
return null;
}
JavaScriptネスト配列・オブジェクトの処理(合計・抽出・変換)
多段ネストの合計(配列と数値の混在)
入れ子の配列を辿りながら、数値だけ足し合わせます。要素が配列なら“再帰で潜る”、数値なら“足す”。
function deepSum(x) {
if (Array.isArray(x)) {
return x.reduce((acc, v) => acc + deepSum(v), 0);
}
return typeof x === "number" ? x : 0;
}
deepSum([1, [2, [3, 4]], "x"]); // 10(文字列は無視)
JavaScriptネストオブジェクトからパスで取得
安全に辿り、途中欠損なら undefined を返します。?. を使わない純粋再帰版です。
function getPath(obj, path) {
if (path.length === 0) return obj;
if (obj == null) return undefined;
const [head, ...rest] = path;
return getPath(obj[head], rest);
}
getPath({ a: { b: { c: 1 } } }, ["a", "b", "c"]); // 1
JavaScript全キー・値を平坦化(フラットな辞書へ)
“親キー.子キー”の形式で集約すると、検索や比較が楽になります。
function flatten(obj, prefix = "", out = {}) {
if (obj == null || typeof obj !== "object") {
out[prefix] = obj;
return out;
}
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
flatten(v, key, out);
}
return out;
}
flatten({ user: { name: "Alice", prefs: { theme: "dark" } } });
// { "user.name": "Alice", "user.prefs.theme": "dark" }
JavaScript再帰の安全対策(重要ポイントの深掘り)
循環参照を検出して止める
オブジェクトが自分を指す構造は再帰が無限ループになります。訪問済みを記録して回避します。
function safeWalk(obj, visit, seen = new WeakSet()) {
if (obj == null || typeof obj !== "object") return;
if (seen.has(obj)) return; // 既に訪問済みなら停止
seen.add(obj);
visit(obj);
for (const v of Object.values(obj)) safeWalk(v, visit, seen);
}
// 例
const a = {}; a.self = a;
safeWalk(a, v => { /* ... */ }); // 無限ループしない
JavaScript深さ制限と早期終了
入力が極端に深い場合は最大深度を設けて防御します。エラーを投げるか、打ち切り値を返す設計にします。
function walkLimit(node, visit, depth = 0, maxDepth = 1000) {
if (depth > maxDepth) throw new Error("Too deep");
if (!node) return;
visit(node);
for (const ch of node.children ?? []) {
walkLimit(ch, visit, depth + 1, maxDepth);
}
}
JavaScript再帰が難しければ“明示的スタック”で反復
再帰の代わりに配列でスタックを持てば、コールスタックの上限を避けられます(イテレーティブDFS)。
function walkIter(root, visit) {
const stack = [root];
while (stack.length) {
const node = stack.pop();
if (!node) continue;
visit(node);
const children = node.children ?? [];
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i]); // 逆順で push すると表示順が自然
}
}
}
JavaScript再帰の作り方を身につける(考え方の型)
1. 入力の最小単位を決める(ベースケース)
「空」「プリミティブ」「子が無い」を返す動作を先に書きます。ここが抜けると無限再帰になります。
2. “1ステップ進める”分解方法を決める
配列なら先頭+残り、ツリーなら自分+子、オブジェクトならキーごと、といった“同型の分割”に落とします。
3. 進めた結果を合成する
合計なら足し合わせ、検索なら見つかった値を返し、変換なら新しい構造を作る。合成は reduce/map の発想と相性が良いです。
// 型の例:sum = first + sum(rest)
function sum(arr) {
if (arr.length === 0) return 0;
const [head, ...tail] = arr;
return head + sum(tail);
}
JavaScriptまとめ
再帰は「ベースケースで止め、再帰ケースで1歩進める」を繰り返すシンプルな技法で、ネスト配列・オブジェクト・ツリーの処理に最適です。欠損(null/undefined)は必ずガードし、循環参照は訪問済み(WeakSet)で防ぎ、深い入力には最大深度や反復(明示的スタック)を用意する。合計・検索・変換は“分割→合成”の型に落とすと安定して書ける。これを体に馴染ませれば、初心者でも複雑なネスト構造を短く、安全に、意図どおりに扱えるようになります。
