質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.92%

javascriptで配列→ネスト構造のオブジェクトにする方法

受付中

回答 4

投稿

  • 評価
  • クリップ 1
  • VIEW 429

manusa360

score 0

前提・実現したいこと

javascriptを扱っています。

こういった配列データがあるとします。

var datas = [
  {
    id: 0,
    parent: null
  },
  {
    id: 1,
    parent:0
  },
  {
    id: 2,
    parent:0
  },
  {
    id: 3,
    parent:1
  },
  {
    id: 4,
    parent: 1
  },
  {
    id: 5,
    parent: 2
  }
]

```

これをネスト構造のオブジェクトに構築したいです。

var tree = [{
  id: 0,
  children: [
    {
      id: 1,
      children: [
        {
          id: 3,
          children: []
        },
        {
          id: 4,
          children: []
        }
      ]
    }, {
      id: 2,
      children: [
        {
          id: 5,
          children: []
        }
      ]
    }
  ]
}]

```

再帰関数を作ればできるかと思い、以下の方法を試しましたが最後詰まってしまいました。

良い解決方法はあるでしょうか。

試したこと

datasは順々に0番目からtreeに押し込まれていきます。

まずdatas[0]は、{id:0,parent:null}は親がいませんからtreeにそのまま押し込まれます。(この処理だけ例外的なので、ここの処理は後から考えます)

tree=[
 {id:0,
  children:[]
 }
]

```

ここからが再帰関数処理の考え所です。

datas[1] {id:1,parent:0}は、treeの中から親となるid:0を探し出して、id:0のchildrenに自分自身を押し込みます。 datas[2]も同様に、treeの中から親となるid:0を探し出して、id:0のchildrenに自分自身を押し込みます。

ここまでの動きを関数で示すと以下の通りです。

function createTree(){
  return datas.reduce((acc,cur) => { 
    var parent = tree.find(e=>e.id==cur.id)
    parent.children.push(cur)
    return acc
  },[])
}

```

今のところ、treeは以下のように出来上がっているはずです。

var tree = [{
    id: 0,
    children: [
      {
        id: 1,
        children: []
      },{
        id: 2,
        children: []
      }
    ]
  }]

```

ここまでは良いのですが、datas[3]はparentがid:1のため、tree.findでは見つけるべき親を見つけられず、treeから親を見つけるためには以下のように記述する必要があります。

tree[0].children.find(e=>e.id==cur.id)

```

このようにreduceのtree.findではネストの深部まで見つけられないため、深部まで探って見つけてくるような関数を実装してやります。 新たな関数名をdeepFindとしました。 ひとまずはfindと同じ機能となるよう実装します。

function deepFind(tree,cur){
  var result=tree.find(e=>e.id==cur.id)
  return result
}

//以下と同じ機能
result=tree.find(e=>e.id==cur.id)

```

さて、deepFind関数を実装していくにあたり、datas[4]をどのように処理していくか考えましょう。 ひとまず、そのままではresultは何も返って来ないので、undefinedの時は次の層を探しにいくような処理に変えます

function deepFind(tree,cur){
  var result=tree.find(e=>e.id==cur.id)
+ if(result){
+   return result
+ }else{
+   result=tree[0].children.find(e=>e.id==cur.id)
+   return result
+ }
}

```

この状態でdatas[5]までは対応できます。 しかしdatas[6]の親はtree[0].childrenからは見つけれこれず、tree[1].childrenから見つけてくる必要があります。

function deepFind(tree,cur){
  var result=tree.find(e=>e.id==cur.id)
  if(result){
    return result
  }else{
    result=tree[0].children.find(e=>e.id==cur.id)
+   if(result){
+     return result
+   }else{
+     rusult=tree[1].children.find(e=>e.id==cur.id)
+     if(result){
+       return result
+     }else{
+       //以下同じように続いていく
+     }
+   }
  }
}

```

さて、ここまで書いていて気づくかもしれませんが、この関数は「resultがあればresultを返し、なければ探す範囲を変えて同じようにfindメソッドで検索していく」という流れです。

ということは、function deepFind(tree,cur)のtreeの部分さえ、次の探す範囲に書き換えてやればif文を無限に書く必要はなくなるわけです。

function deepFind(tree,cur){
  var result=tree.find(e=>e.id==cur.id)
  if(result){
    return result
  }else{
+   return deepFind(*次の検索範囲*,cur)
  }
}

```

探す範囲がtreeなのは最初の一発だけなので、もっと一般的な意味あいでtargetという言葉に書き換えてやります。 ついでに探すキーワードをcurからsubjectに書き換えます。

function deepFind(target,subject){
  var result=target.find(e=>e.id==subject.id)
  if(result){
    return result
  }else{
    return deepFind(*次の検索範囲*,subject)
  }
}

```

さて、この次の検索範囲というのはどのように指定すれば良いのでしょうか。

data[1]とdata[2]ではtreeが、 datas[3]とdatas[4]ではtree[0].children、datas[5]ではtree[1].childrenが探す範囲でしたので、以下のようにすれば良さそうです。

var idx=0;
function deepFind(target,subject){
  var result=target.find(e=>e.id==subject.id)
  if(result){
    return result
  }else{
    idx++
    var nextTarget=tree[idx].children
    return deepFind(nextTarget,subject)
  }
}

```

ただこの記述だと、ネストが1階層は対応できますが、もっと深いネストになった時に対応できません。

treeのネストが深くなっていった時に、どうやったら抜け漏れなく親を探せるか考えます。

0①--1②--3③--5④
  |   |   |-6④
  |   |-4③--7⑤
  |       |-8⑤
  |-2②--9⑥

検索範囲の指定方法
①tree
②tree[0].children
③tree[0].children[0].children
④tree[0].children[0].children[0].children  //これ以上深いネストはない
⑤tree[0].children[0].children[1].children //これより下に兄弟ネストはない
⑥tree[0].children[1]

```

このように深いネストの時はtargetに[0].childrenを追加し、兄弟ネストに移る時は0にプラス1にすれば良いわけです。

したがってロジックとしては、 今探しているところに親が見つからない →もう一段階深いネストがあれば、そこを検索 →それ以上深いネストがない時は、インデックスにプラス1して兄弟ネストを検索する →兄弟ネストもない時は、一段階浅いネストに移動して兄弟ネストを検索する(その時インデックスは0に戻す) という流れにすると良さそうです。

function deepFind(target,subject){
  var result=target.find(e=>e.id==subject.id)
  if(result){
    return result
  }else{
    //もう一段階深いネストがあれば、そこが次の検索ターゲット
    if(target[0].children.length){
      var nextTarget=target[0].children
    } else { 
      //深いネストがない場合は弟ネストを検索する。
      if(idx+1<target.length){  //弟がいるか。idx+1がtarget.lengthと同じ値の時には弟はいない
        var nextTarget=target[idx+1].children
      }
    } else {
    //兄弟ネストもない時は、一段階浅いネストに移動してその弟ネストを検索する
    //このやり方がわかりません
    }
    return deepFind(nextTarget,subject)
  }
}

```

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • think49

    2020/09/13 22:27

    > var datas = [

    data(複数形)、datum(単数形)ですので、"s" は不要です。

    キャンセル

  • Zuishin

    2020/09/15 10:11

    回答がすでにいくつもついています。何を待っていますか?
    自分の欲しい回答がないのであれば、何が足りないのかを書かなければ回答者には伝わりません。

    知りたいのはネスト構造のオブジェクトを作ることではなく、あくまでも次の検索範囲の指定のしかたということですか?

    キャンセル

  • manusa360

    2020/09/15 20:15

    回答ありがとうございます。
    (すみません、昨日はteratailを確認できておりませんでした。)

    think49様がクラス化までコーディングしていただきましたので、コードで望みの動きができるか確認いたします。

    キャンセル

回答 4

+4

こんにちは

ご質問にありますように、与えられる配列datas が、各要素の parent の昇順(nullを先頭として、以降、0, 1, 2 …)にソートされていることを前提として、ツリーを作っていくのと併せて、各ノードの id とノードオブジェクトのマップも作ってparent のノードを得るときにこれを使うようにすれば、再帰を使わずに、 tree が得られるのではと思います。

const [tree] = datas.reduce(([ary, map], e) => {
  const node = { id: e.id, children: [] };
  if (e.parent === null) {
    ary.push(node);
  } else if (map[e.parent]) {
    map[e.parent].children.push(node);
  }
  map[e.id] = node;
  return [ary, map];
}, [[], []]);

上記では、 ツリーノードのidとノードオブジェクトとのマップを配列で実装しており、それが、reduce の各回で変数mapに入ってきます。 

参考になれば幸いです。

追記

参考までに、再帰を使って、指定idのノードをツリーから深さ優先で探すコードを挙げます。(※再帰の深さが何らかの最大値を越えないようする処理は加えていません。)

const findTreeNodeById = (tree, id) => {
  for (let i = 0; i < tree.length; ++ i) {
    if (tree[i].id === id) {
      return tree[i];
    } else if (tree[i].children.length > 0) {
      const node = findTreeNodeById(tree[i].children, id);
      if (node) {
        return node;
      }
    }
  }
  return null;
}

追記2

一点、細かい指摘にはなりますが、補足があります。ご質問では

var tree = [{
 id: 0,

となっており、最終的な結果を得る変数 tree は配列になっており、これに合わせて、先に挙げた回答コードで得られる tree も配列になりますが、一般にツリー(木構造)というと、ルートのノードが一個と考えるのが通常と思われます。(ちなみに、グラフ理論の用語では、複数の木は、森(forest)と呼ばれます。) これをふまえて、もし、元の配列 datasの要素として、 parent が null である要素は一個しか出現しないことが前提になるのでしたら、最終的な結果が入る変数tree には、配列ではなくルートノードのオブジェクトが入ってくることにしたほうがよいかもしれません。または、元の配列 datasの要素として、 parent が null である要素が複数ある場合は、最終的な結果は配列になり、その要素それぞれが木のルートノードになるので、変数名を複数形のtrees にするか、あるいは forest にしたほうがよいかもしれません。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/09/15 20:00

    ご回答ありがとうございます。

    nodeの上から探して行って、そこの層に見つからなければ、深い層を探していく・・・というやり方ですね。

    ただツリーが複数あるような場合では、あるツリーを深くまで探して行って結局見つからなくなった場合に、浅い層まで戻って別の系統を深くまで探していく、、、というやり方ができないため、ツリー内をくまなく探せるアルゴリズムを探索しています。

    ※追記2でおっしゃられるように、datasにはparentがnullの要素を複数入れますので、最終的にツリーを格納する変数は「trees」にします。

    キャンセル

  • 2020/09/15 23:59 編集

    このコードは複数のツリーに対応した深さ優先探索で、すべてのツリー内をくまなく探せます。

    「深さ優先探索」というのはアルゴリズムの名前です。
    https://ja.wikipedia.org/wiki/%E6%B7%B1%E3%81%95%E5%84%AA%E5%85%88%E6%8E%A2%E7%B4%A2

    キャンセル

  • 2020/09/16 18:46

    @manusa360さん

    > ただツリーが複数あるような場合では、
    > ・・・
    > ツリー内をくまなく探せるアルゴリズムを探索しています。

    なるほどです。そのお話ですと、単純な再帰では解決できないような、ちょっと込み入った課題に挑戦されているようですね。うまく問題解決できることを祈念申し上げます。

    キャンセル

  • 2020/09/16 18:53 編集

    @Zuishinさん
    フォローありがとうございます。
    Wikipedia に「深さ優先探索」があったとは失念しておりました。「深さ優先探索のイメージ」の絵が分かりやすいですし「幅優先探索」へのリンクもあるので両者を対照しやすくていいですね。

    キャンセル

+4

new Map

検索効率が高いマップを利用するとして、
「常にparentがchildrenに先行する条件」なら、@jun68yktさんと同じ方向性を。
順序に規則性がないなら、マップ生成処理を独立させます。

'use strict';
const src = [{id:0,parent:null},{id:1,parent:0},{id:2,parent:0},{id:3,parent:1},{id:4,parent:1},{id:5,parent:2}];

function createTree1 (array) {
  return array.reduce(function (tree, current) {
    const parent = current.parent;
    parent === null ? tree.push(this.get(current.id)) : this.get(parent).children.push(this.get(current.id));
    return tree;
  }.bind(new Map(array.map(obj => [obj.id, {id:obj.id,children:[]}]))), []);
}

console.dir(createTree1(src));

class

DOM APIのように、API拡張を目指すなら、class化するのが望ましいと思います。

'use strict';
const src = [{id:0,parent:null},{id:1,parent:0},{id:2,parent:0},{id:3,parent:1},{id:4,parent:1},{id:5,parent:2}];

class Elm {
  constructor (id, children) {
    this.id = id;
    this.children = children ? Array.from(children) : [];
  }
}

Object.defineProperties(Elm.prototype, {
  appendChild: {
    writable: true,
    enumerable: false,
    configurable: true,
    value: function appendChild (...children) {
      return Array.prototype.push.call(this, ...children);
    }
  }
});

class DF {}
Object.defineProperties(DF.prototype, {
  [Symbol.iterator]: {writable: true, enumerable: false, configurable: true, value: Array.prototype[Symbol.iterator]},
  length: {writable: true, enumerable: true, configurable: true, value: 0},
  appendChild: {
    writable: true,
    enumerable: false,
    configurable: true,
    value: Elm.prototype.appendChild
  }
});

function createTree2 (array) {
  return array.reduce(function (df, current) {
    const parent = current.parent;
    parent === null ? df.appendChild(this.get(current.id)) : this.get(parent).appendChild(this.get(current.id));
    return df;
  }.bind(new Map(array.map(obj => [obj.id, new Elm(obj.id)]))),
   new DF);
}

console.dir(createTree2(src));

オブジェクト初期化子

@Zuishin さんのコメントに対しての返答。
(長文なので、回答に追記します)

以前も気になったんですが、オブジェクト「初期化子」とオブジェクトを混同していませんか?

私は「オブジェクト初期化子を使用してObject型のデータを初期化する」の意味で「オブジェクト初期化子を使用」と表現しています。

(1) オブジェクト
この文脈で「オブジェクト」と表現すると、

  • new Object と受け取る人
  • Object 型のデータと受け取る人

に分類されて解釈に違いが現れる為、曖昧な表現を私は避けます。
(私が読者なら、言葉通りに後者で解釈する事を試み、文脈を読み取って適宜読み替えます)
私は「new Object」「オブジェクト初期化子」「Object型」を明確に使い分ける性質です。

(2) オブジェクト初期化子とオブジェクト
「オブジェクト(Object型のデータ)」と「オブジェクト初期化子」では生成されるオブジェクトの範囲がまるで違います。
new Objectもnew Arrayも同様にObject型ですが、生成されたオブジェクトが持つ性質は全く違ったものになります。

  • (A)「new Map」と「オブジェクト初期化子」
  • (B)「new Map」と「オブジェクト」

今回、私は (A) で対比しましたが、仮に (B) で対比された場合、私は「new Mapもオブジェクトなので、両者は比較対象になりません」と突っ込みを入れるでしょう。

(3) 実用性
単純な「Object型のデータの入れ物」として扱う場合は、new Object にメリットはほとんどなく、

Object.create(null)

がより汎用的です。
この場合、「プロパティに整数値のみを使用している為、オブジェクト初期化子(new Object)でも実質的に問題はない」等、採用理由がある場合もあります。
生成されたオブジェクトの性質を正確に示すためにも「オブジェクト初期化子でオブジェクトを生成していること」を私は強調します。

Re: manusa360 さん

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/09/17 09:13

    > マップにnew Mapではなく、オブジェクト初期化子を利用している点を除けば、

    > JavaScriptに「連想配列」の定義はなく、別言語の定義を引きずって意図とは別の意味を含んだ解釈をする人がいるので、私は避けています。

    「連想配列」ではなく「マップ」なら良いということですか?
    「マップ」は Map オブジェクトあるいは Array.prototype.map() のことではないでしょうか。

    ここで「マップ」と呼ばれたものは「オブジェクト初期化子」でも「new Map」でもないことは明白です。

    > 「オブジェクト初期化子」は文法上の言葉ですが、生成されるオブジェクトは確定しています。
    > {} と書いても、new Object と書いても、生成される値は全く変わりません。
    > そこに誤解が生じる可能性を私は感じていません。

    {} はオブジェクト初期化子ですが、new Object はそうではありません。
    そこで生成されるものを問題にしているのならば、やはりインスタンスをオブジェクト初期化子と呼んでいるようにしか思えません。

    キャンセル

  • 2020/09/17 14:19

    > そこで生成されるものを問題にしているのならば、やはりインスタンスをオブジェクト初期化子と呼んでいるようにしか思えません。

    生成されたオブジェクトに言及したのは、@Zuishinさんから指摘があったことで「@Zuishinさんに返答」しましたが、

    > マップにnew Mapではなく、オブジェクト初期化子を利用している点を除けば、

    この一文にインスタンスの意図は全くありません。
    むしろ、この一文でインスタンスと読み取る方が私は理解に苦しみます。
    どう読んでも、この一文ではインスタンスと解釈出来ませんが、どういう論理でインスタンスと読めるのでしょう?

    私が行ったのは、new Mapと{}を比較出来るようにヒントを与えただけで、それ以降の考察は質問者自身がやるべき事です。
    元々の質問では「マップ」に言及してませんから、これは回答ではありません。

    真面目に回答するならば、「質問者が理解してないこと」「質問者が調査したこと」を明らかにした上で回答を行うべきです。
    質問者の理解度を勝手に考察して「ここまで説明すべき」とするのはエスパーを期待しすぎと考えます。

    キャンセル

  • 2020/09/17 14:40

    意図は全くありませんと言われても、おかしな日本語です。「マップ」が Map のことでないなら何のことなんでしょうか?

    キャンセル

+3

再帰要りませんでした。

var datas = [
    {
      id: 0,
      parent: null
    },
    {
      id: 1,
      parent:0
    },
    {
      id: 2,
      parent:0
    },
    {
      id: 3,
      parent:1
    },
    {
      id: 4,
      parent: 1
    },
    {
      id: 5,
      parent: 2
    }
  ];

const result = datas.map(a => { return {"id":a.id, "children":[]}; });
for (const datum of datas.filter(a => a.parent !== null)) {
    result[datum.parent].children.push(result[datum.id]);
}
const tree = [result[0]];
console.log(JSON.stringify(tree, undefined, 2));

追記 id が連番でない、またはルートが複数ある場合

const result = datas
    .map(a => { return {"id":a.id, "children":[]}; })
    .reduce((result, item) => { result[item.id] = item; return result; }, {});
for (const datum of datas.filter(a => a.parent !== null)) {
    result[datum.parent].children.push(result[datum.id]);
}
const  tree = datas
    .filter(a => a.parent === null)
    .map(a => result[a.id]);
console.log(JSON.stringify(tree, undefined, 2));

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/09/15 20:45

    > 追記 id が連番でない、またはルートが複数ある場合

    今更ですが、「ルートが複数ある」は違和感のある表現ですね。
    それはもうルートではないのでは…。

    キャンセル

  • 2020/09/15 20:55 編集

    木が複数ある場合という意味です。tree がなぜか配列になっていたので。

    キャンセル

  • 2020/09/15 21:09

    ああ、なるほど。
    関数「createTrees」なんですね。

    キャンセル

+1

DOMライクに ノードを操作する手法が後々楽なのではないでしょうか。

この場合、重要なのは親子関係です。
parent には 親id ではなく、親ノードの参照を保持すると、children 以外に、sibilings の探索 closest() も探索しやすいプロパティをもたせることができます。

class Node {

  constructor ( id ) {
    this.parent = null
    this.id = id;
    this.children = [];
  }
  hasChildNodes () {
    return !!this.children.length;
  }
  appendChild( node ) {
    if( Node.isNode(node) ) {
      node.parent = this; // parent 
      this.children.push( node );
    }
  }
  removeChild( node ) {
    if( this.parent && Node.isNode(node) ) {
      let idx = this.children.indexOf( node );
      if( ~idx ) {
        this.children.splice( idx, 1 );
        node.parent = null; // parent
      }
    }
  }
  getNodeById ( id ) {
    if( this.id === id ) return this;
    let rslt;
    if( this.hasChildNodes() ) {
      this.children.some( child => (rslt = child).id === id );
    }
    return rslt;
  }

  toJson () {
    let
      { id, children } = this,
      childNodes = []
    ;
    if( this.hasChildNodes() ) {
      children.forEach( child => childNodes.push( child.toJson() ) );
      children = childNodes;
    }
    return { id, children };
  }

  static isNode( node ) {
    return node instanceof Node;
  }

  static fromDatas ( datas ) {
    let rootNode;
    for ( let {id, parent} of datas ) {
      let
        node = new Node( id ),
        parentNode = null
      ;
      if( !rootNode ) {
        rootNode = new Node(id);
      }
      else if( parent!==null ) {
        parentNode = rootNode.getNodeById( parent );
        parentNode.appendChild( node );
      }
    }
    return rootNode;
  }
}



var datas = [
  { id: 0, parent: null },
  { id: 1, parent: 0 },
  { id: 2, parent: 0 },
  { id: 3, parent: 1 },
  { id: 4, parent: 1 },
  { id: 5, parent: 2 }
];
var tree1 = Node.fromDatas(datas).toJson();

console.log( tree1 ); // { id, parent, children } : ルートノード

/*
0①--1②--3③--5④
  |   |   |-6④
  |   |-4③--7⑤
  |       |-8⑤
  |-2②--9⑥
 */
var datas2 = [
  { id: 0, parent: null },
  { id: 1, parent: 0 },
  { id: 2, parent: 0 },
  { id: 3, parent: 1 },
  { id: 4, parent: 1 },
  { id: 5, parent: 3 },
  { id: 6, parent: 3 },
  { id: 7, parent: 4 },
  { id: 8, parent: 4 },
  { id: 9, parent: 2 }
];
var tree2 = Node.fromDatas(datas2);
console.log( tree2 ); // { id, parent, children } : ルートノード

var tree = [ tree2.toJson() ]; // 探索用に collection ライクにする。
console.log( tree );
console.log( tree[0].children );
console.log( tree[0].children[0].children );
console.log( tree[0].children[0].children[0].children );
console.log( tree[0].children[0].children[1].children );
console.log( tree[0].children[1] );

console.log( JSON.stringify( tree, null, 2) )

追記)

DOM ライクな実装は「ルートが1つ」に制限されますが、「文書断片を意味するノードをルートとする」ことを考えれば、その断片がもつ children を tree の Collection とするアイディアになります。
(上記回答では未実装です)

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/09/16 22:23

    > DOM ライクな実装は「ルートが1つ」に制限されますが、「文書断片を意味するノードをルートとする」ことを考えれば、その断片がもつ children を tree の Collection とするアイディアになります。

    私は DOM Interface の NodeList, HTMLCollection, DocumentFragmentのInterface模倣が終着点と考えました。

    私の回答コードでは DocumentFragment を参考にしました。
    appendChildは可変長引数に変更しましたが…。

    キャンセル

  • 2020/09/17 07:24

    私の回答では 様々に派生するNode を参考にしています。
    DOM lv2が実用されていた頃に JsonML element で DOMを偽装しようと試みたことがあるのですが、「ノードを抽象化してツリーを構成する場合、親子関係が重要」であるとの経験則が私の回答になります。

    JsonML では Document、DocumentFragment、NodeList、HTMLCollection はどれも tagName を "" として、内容の異なる子要素の構成で使い分け(仕様とは全く異なる扱い方)をしなければ、使いづらかった。
    「仕様はあるのだけど、非公開の個人利用なら問題ないよね」という大雑把な考え方です…。
    (nodeType も無理やり増やしたりしていました)

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.92%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る