JavaScript
 Computer >> コンピューター >  >> プログラミング >> JavaScript

JavaScriptで親子関係を持つ配列をレベル別にツリー構造へ変換する方法

親子関係を持つ配列をツリー構造へ変換する

同一の配列の中に1対多の親子関係を持つデータを扱う場面は少なくありません。たとえば組織図のようなデータでは、階層が「レベル(level)」によって定義され、各要素の親は必ず自分より1段上のレベルに存在し、parentIdプロパティで参照されます。

本記事では、このようなフラットな配列から多階層のツリー構造(ネストされた配列)を生成する方法を解説します。最上位レベルの要素がメイン(ルート)となり、その子要素はchildrenというサブ配列として格納されます。

入力データの例

入力配列が次のように与えられたとします。

const arr = [
    {
        _id: 100,
        level: 3,
        parentId: null,
    },
    {
        _id: 101,
        level: 2,
        parentId: 100,
    },
    {
        _id: 102,
        level: 2,
        parentId: 100,
    },
    {
        _id: 103,
        level: 2,
        parentId: 100,
    },
    {
        _id: 104,
        level: 1,
        parentId: 101,
    },
    {
        _id: 105,
        level: 1,
        parentId: 102,
    },
    {
        _id: 106,
        level: 1,
        parentId: 101,
    },
    {
        _id: 107,
        level: 1,
        parentId: 103,
    },
    {
        _id: 108,
        level: 1,
        parentId: 102,
    },
    {
        _id: 109,
        level: 1,
        parentId: 103,
    }
];

この入力から得られるべき出力は、次のようなツリー構造です。

100
├── 101
│   ├── 104
│   └── 106
├── 102
│   ├── 105
│   └── 108
└── 103
    ├── 107
    └── 109

実装コード

この変換を行うコードは次のようになります。

// 前述の arr を入力として使用します
const prepareTree = (arr = [], root = null) => {
    let res;
    // プロトタイプを持たない空のマップを生成
    const obj = Object.create(null);
    arr.forEach(el => {
        // 同じIDの仮オブジェクトが既にあれば、その children を引き継ぐ
        el.children = obj[el._id] && obj[el._id].children;
        obj[el._id] = el;
        if (el.parentId === root) {
            // parentId が null の要素がルート
            res = el;
        }
        else {
            // 親がまだ実体化していなければ仮オブジェクトを作る
            obj[el.parentId] = obj[el.parentId] || {};
            obj[el.parentId].children = obj[el.parentId].children || [];
            obj[el.parentId].children.push(el);
        }
    });
    return res;
};

console.log(JSON.stringify(prepareTree(arr), undefined, 4));

アルゴリズムの仕組み

このアプローチのポイントは、以下の通りです。

  • Object.create(null)で生成したプレーンなマップに、各要素を_idをキーとして順に登録していきます。
  • 要素を処理する際、同じIDがマップに既に存在する場合(=子要素が先に現れ、親の仮オブジェクトが作られていた場合)は、既存のchildrenをそのまま引き継ぎます。
  • parentIdnullと一致する要素はルートとして、結果変数resに保持されます。
  • それ以外の要素は、親IDをキーとするマップのエントリ(存在しなければ新規作成)のchildren配列に追加されます。

すべての子要素は親オブジェクトへの参照を共有しているため、親が配列の後半に登場してもリンクは自動的に維持されます。その結果、配列を一度だけ走査するだけで済み、計算量はO(n)ときわめて効率的です。

出力結果

コンソールに出力されるJSONは次のようになります。

{
    "_id": 100,
    "level": 3,
    "parentId": null,
    "children": [
        {
            "_id": 101,
            "level": 2,
            "parentId": 100,
            "children": [
                {
                    "_id": 104,
                    "level": 1,
                    "parentId": 101
                },
                {
                    "_id": 106,
                    "level": 1,
                    "parentId": 101
                }
            ]
        },
        {
            "_id": 102,
            "level": 2,
            "parentId": 100,
            "children": [
                {
                    "_id": 105,
                    "level": 1,
                    "parentId": 102
                },
                {
                    "_id": 108,
                    "level": 1,
                    "parentId": 102
                }
            ]
        },
        {
            "_id": 103,
            "level": 2,
            "parentId": 100,
            "children": [
                {
                    "_id": 107,
                    "level": 1,
                    "parentId": 103
                },
                {
                    "_id": 109,
                    "level": 1,
                    "parentId": 103
                }
            ]
        }
    ]
}

この手法は、カテゴリ一覧、コメントスレッド、組織図など、データベースから取得したフラットな行データをUI向けのネスト構造へ変換したい場面で幅広く活用できます。

  1. JavaScriptのslice()メソッドとは?配列から要素を取り出す使い方を実例で解説

    JavaScriptのslice()メソッドは、大きな配列の中から選択した範囲の要素を抜き出し、新しい配列として返すメソッドです。最大の特徴は、元の配列を一切変更しない(非破壊的な操作)という点にあります。そのため、元データを保持したまま部分的なコピーを作りたい場合に非常に便利です。 slice()メソッドの基本構文 arr.slice(start, end) start:取り出しを開始するインデックス番号(省略可・デフォルトは0) end:取り出しを終了する位置の直前のインデックス番号(省略可・デフォルトは配列の末尾まで)。endで指定した要素自体は結果に含まれない点に注意してください。

  2. JavaScriptのshift()メソッドとは?配列の先頭要素を削除する方法を解説

    JavaScriptのshift()メソッドとはJavaScriptのshift()メソッドは、配列から先頭(最初)の要素を取り除き、その要素を返すメソッドです。この操作により配列の長さは1つ減少します。なお、末尾の要素を削除したい場合はpop()メソッドを使用し、先頭に要素を追加したい場合はunshift()メソッドを使います。shift()メソッドのコード例以下は、ボタンをクリックすると配列の先頭要素が削除されるサンプルコードです。<!DOCTYPE html> <html lang="ja"> <head> <meta cha