JavaScript | 配列・オブジェクト:ネスト構造の扱い – 再帰処理の基礎

JavaScript JavaScript
スポンサーリンク

再帰処理とは何か

再帰は「関数が自分自身を呼び出して、問題を“同じ形のより小さな問題”に分解して解く」テクニックです。ここが重要です:必ず“止まる条件(ベースケース)”を先に書き、次に“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)で防ぎ、深い入力には最大深度や反復(明示的スタック)を用意する。合計・検索・変換は“分割→合成”の型に落とすと安定して書ける。これを体に馴染ませれば、初心者でも複雑なネスト構造を短く、安全に、意図どおりに扱えるようになります。

タイトルとURLをコピーしました