JavaScript | 再帰を使わないと書きにくい実世界の例(DOMツリーやJSONの処理)

JavaScript JavaScript
スポンサーリンク

実務で「再帰が自然で書きやすい」代表例を具体的な説明+実用的なコード例(再帰版と必要なら非再帰版のワンポイント)でまとめます。JavaScript(ブラウザ / Node)でそのまま試せる形にしています。使いどころ・利点・注意点も書くので、すぐ応用できます。

1) DOMツリー操作(ブラウザ)

用途例:複雑な DOM を全検索して条件に合う要素へ処理、あるいは階層情報(深さ/パス)を計算する時。

再帰が自然な理由:DOM は本質的にツリー構造。各ノードに対して「同じ処理」を子ノードに適用するパターンがそのまま再帰になる。

例:指定したクラス名を持つ要素を深さ優先で全取得(再帰):

function findByClass(node, className, out = []) {
  if (node.classList && node.classList.contains(className)) {
    out.push(node);
  }
  for (let child of node.children) {
    findByClass(child, className, out);
  }
  return out;
}

// 使い方(ページ全体)
const matches = findByClass(document.documentElement, "target-class");
console.log(matches);
JavaScript

非再帰(スタックを使う)ワンポイント:

function findByClassIter(root, className) {
  const stack = [root];
  const out = [];
  while (stack.length) {
    const node = stack.pop();
    if (node.classList && node.classList.contains(className)) out.push(node);
    for (let i = node.children.length - 1; i >= 0; i--) stack.push(node.children[i]);
  }
  return out;
}
JavaScript

→ 再帰よりスタック版はコールスタック問題を回避できるが、コードが少し冗長になる。


2) ネストした JSON の検索・集約(API レスポンスや設定ファイル)

用途例:JSON の深いネストから特定キー/値を探したり、特定フォーマットのフィールドだけ抽出する。

再帰が自然な理由:JSON は入れ子(配列/オブジェクト)の再帰的構造。鍵を辿っていく処理はそのまま自己呼び出しで表現できる。

例:オブジェクトのどこかにある特定のキー id を全て集める:

function collectIds(obj, out = []) {
  if (obj && typeof obj === "object") {
    if ("id" in obj) out.push(obj.id);
    for (const k in obj) {
      collectIds(obj[k], out);
    }
  }
  return out;
}

// テスト
const data = { id: 1, items: [{id:2, children:[{id:3}]}] };
console.log(collectIds(data)); // [1,2,3]
JavaScript

注意点:循環参照(self-referential object)があり得る場合は、WeakSet 等で訪問済みチェックを入れること。


3) ツリー表示/レンダリング(React 等の UI)

用途例:ネストしたメニューやコメントツリーをコンポーネントでレンダリングする。

再帰が自然な理由:各ノードは「子ノードを同じコンポーネントで描画する」ためコードが非常に簡潔になる。

React の例(再帰コンポーネント):

// NodeItem.jsx
export default function NodeItem({ node }) {
  return (
    <li>
      {node.title}
      {node.children && node.children.length > 0 && (
        <ul>
          {node.children.map(child => (
            <NodeItem key={child.id} node={child} />
          ))}
        </ul>
      )}
    </li>
  );
}
JSX

→ これだけで深さ無制限のツリーが描ける。非再帰で同じことをするのはJSXの生成ロジックが煩雑になる。

注意:非常に深いツリーだとブラウザのレンダリングやReactの再帰が問題になることがある(パフォーマンス見積もりを)。


4) ファイルシステムの再帰的探索(Node.js)

用途例:ディレクトリを再帰してファイルを列挙、特定拡張子を検索する CLI ツール。

再帰が自然な理由:ディレクトリは階層構造そのもの。

Node の同期例(理解用):

const fs = require('fs');
const path = require('path');

function walkDir(dir, cb) {
  const entries = fs.readdirSync(dir, { withFileTypes: true });
  for (const entry of entries) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      walkDir(full, cb);
    } else {
      cb(full);
    }
  }
}

// 使い方
walkDir("./", filePath => console.log(filePath));
JavaScript

非同期/ストリーミングや巨大ディレクトリでは再帰の深さとI/O量に注意。非再帰(キュー)で実装する選択肢もある。


5) 構文木(AST)操作・コード解析・変換

用途例:コンパイラやトランスパイラで AST を辿って最適化や変換を行う(例:Babel プラグイン、静的解析)。

再帰が自然な理由:AST は再帰的ノード構造。ノードごとに処理をし、子ノードに同じ処理を適用する。

簡単な例(疑似コード):

function visit(node) {
  // ノードの種類に応じた処理
  if (node.type === 'BinaryExpression') {
    // 左右を再帰で訪問
    visit(node.left);
    visit(node.right);
  } else if (node.type === 'FunctionDeclaration') {
    for (const stmt of node.body) visit(stmt);
  }
  // ... 他ノード
}
JavaScript

→ 再帰で書くと変換ルールが読みやすく保守的。


6) ネストしたバリデーション(フォーム/JSON スキーマ)

用途例:入れ子になったフォームデータや設定を再帰的に検証(必須チェック、型チェック、条件付き必須など)。

再帰が自然な理由:属性や子要素に同じバリデーションルールを適用するため。

例(シンプルな再帰バリデータ):

function validate(node, errors = []) {
  if (node.required && (node.value === undefined || node.value === "")) {
    errors.push(`${node.name} is required`);
  }
  if (node.children) {
    for (const child of node.children) validate(child, errors);
  }
  return errors;
}
JavaScript

7) 深いコピー(Deep clone)や差分計算

用途例:オブジェクト/配列を再帰的に複製、または差分(diff)を取る。

再帰が自然な理由:入れ子構造を個別に複製/比較していく必要があるため。

簡易 deep clone:

function deepClone(obj, seen = new WeakMap()) {
  if (obj === null || typeof obj !== "object") return obj;
  if (seen.has(obj)) return seen.get(obj); // 循環対応
  const out = Array.isArray(obj) ? [] : {};
  seen.set(obj, out);
  for (const k in obj) out[k] = deepClone(obj[k], seen);
  return out;
}
JavaScript

注意:関数や特殊オブジェクト(Date, RegExp, Map, Set など)は別途ハンドリングが必要。


実務での「再帰を選ぶ基準」と注意点(まとめ)

  • 選ぶ理由:データが自然にツリー/入れ子になっていて「同じ処理を子に適用する」パターンが明確なとき。コードが簡潔で読みやすくなる。
  • 性能上の注意:深い入れ子(数千階層など)ではコールスタックが限界になりやすい → スタックを自前で使う反復実装に置き換える。
  • 安全対策:循環参照があり得るデータなら訪問済みチェック(Set/WeakSet)を入れる。
  • デバッグconsole.log で入出力を追うか、ブラウザのデバッガでコールスタックを観察すると流れが掴みやすい。
  • UI(React等)での注意:再帰コンポーネントは便利だが、レンダリング性能とキーの付け方(key)に気をつける。

練習課題(おすすめ)

  1. ブラウザで DOM ツリーを再帰で探索し、すべての img タグの src を配列にして返す。
  2. ネストした JSON から type:"error" のオブジェクトをすべて抽出する。
  3. React コンポーネントで再帰的にネストされたコメントツリーを表示する(reply 機能付き)。
  4. Node.js でディレクトリを再帰して .log ファイルだけを列挙する CLI を作る(循環は無いが大量ファイルに注意)。
タイトルとURLをコピーしました