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

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

新規登録して質問してみよう
ただいま回答率
85.35%
JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Q&A

4回答

4774閲覧

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

manusa360

総合スコア0

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

0グッド

1クリップ

投稿2020/09/13 09:33

前提・実現したいこと

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と同じ機能となるよう実装します。

javascript

1function deepFind(tree,cur){ 2 var result=tree.find(e=>e.id==cur.id) 3 return result 4} 5 6//以下と同じ機能 7result=tree.find(e=>e.id==cur.id)

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

javascript

1function deepFind(tree,cur){ 2 var result=tree.find(e=>e.id==cur.id) 3+ if(result){ 4+ return result 5+ }else{ 6+ result=tree[0].children.find(e=>e.id==cur.id) 7+ return result 8+ } 9}

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

javascript

1function deepFind(tree,cur){ 2 var result=tree.find(e=>e.id==cur.id) 3 if(result){ 4 return result 5 }else{ 6 result=tree[0].children.find(e=>e.id==cur.id) 7+ if(result){ 8+ return result 9+ }else{ 10+ rusult=tree[1].children.find(e=>e.id==cur.id) 11+ if(result){ 12+ return result 13+ }else{ 14+ //以下同じように続いていく 15+ } 16+ } 17 } 18}

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

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

javascript

1function deepFind(tree,cur){ 2 var result=tree.find(e=>e.id==cur.id) 3 if(result){ 4 return result 5 }else{ 6+ return deepFind(*次の検索範囲*,cur) 7 } 8}

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

javascript

1function deepFind(target,subject){ 2 var result=target.find(e=>e.id==subject.id) 3 if(result){ 4 return result 5 }else{ 6 return deepFind(*次の検索範囲*,subject) 7 } 8}

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

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

javascript

1var idx=0; 2function deepFind(target,subject){ 3 var result=target.find(e=>e.id==subject.id) 4 if(result){ 5 return result 6 }else{ 7 idx++ 8 var nextTarget=tree[idx].children 9 return deepFind(nextTarget,subject) 10 } 11}

ただこの記述だと、ネストが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に戻す)
という流れにすると良さそうです。

javascript

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

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

think49

2020/09/13 13:27

> var datas = [ data(複数形)、datum(単数形)ですので、"s" は不要です。
Zuishin

2020/09/15 01:11

回答がすでにいくつもついています。何を待っていますか? 自分の欲しい回答がないのであれば、何が足りないのかを書かなければ回答者には伝わりません。 知りたいのはネスト構造のオブジェクトを作ることではなく、あくまでも次の検索範囲の指定のしかたということですか?
manusa360

2020/09/15 11:15

回答ありがとうございます。 (すみません、昨日はteratailを確認できておりませんでした。) think49様がクラス化までコーディングしていただきましたので、コードで望みの動きができるか確認いたします。
guest

回答4

0

new Map

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

JavaScript

1'use strict'; 2const 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}]; 3 4function createTree1 (array) { 5 return array.reduce(function (tree, current) { 6 const parent = current.parent; 7 parent === null ? tree.push(this.get(current.id)) : this.get(parent).children.push(this.get(current.id)); 8 return tree; 9 }.bind(new Map(array.map(obj => [obj.id, {id:obj.id,children:[]}]))), []); 10} 11 12console.dir(createTree1(src));

class

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

JavaScript

1'use strict'; 2const 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}]; 3 4class Elm { 5 constructor (id, children) { 6 this.id = id; 7 this.children = children ? Array.from(children) : []; 8 } 9} 10 11Object.defineProperties(Elm.prototype, { 12 appendChild: { 13 writable: true, 14 enumerable: false, 15 configurable: true, 16 value: function appendChild (...children) { 17 return Array.prototype.push.call(this, ...children); 18 } 19 } 20}); 21 22class DF {} 23Object.defineProperties(DF.prototype, { 24 [Symbol.iterator]: {writable: true, enumerable: false, configurable: true, value: Array.prototype[Symbol.iterator]}, 25 length: {writable: true, enumerable: true, configurable: true, value: 0}, 26 appendChild: { 27 writable: true, 28 enumerable: false, 29 configurable: true, 30 value: Elm.prototype.appendChild 31 } 32}); 33 34function createTree2 (array) { 35 return array.reduce(function (df, current) { 36 const parent = current.parent; 37 parent === null ? df.appendChild(this.get(current.id)) : this.get(parent).appendChild(this.get(current.id)); 38 return df; 39 }.bind(new Map(array.map(obj => [obj.id, new Elm(obj.id)]))), 40 new DF); 41} 42 43console.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 にメリットはほとんどなく、

JavaScript

1Object.create(null)

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

Re: manusa360 さん

投稿2020/09/13 13:22

編集2020/09/16 13:59
think49

総合スコア18189

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

manusa360

2020/09/15 11:12

ご回答ありがとうございます。 実装ではparentがchildrenに先行してtreeに組み込まれるとは限らないので、先に親がくるようにdatasをソートしたり、クラス化する必要もあると思っていましたので、ハッシュマップ生成処理のアドバイスをいただきまして、とても助かります。 内容については、すみませんこれまでMapをほとんど使ったことがないので、どういう動きをしているか頂戴しましたコードで確認いたします。
think49

2020/09/15 11:42 編集

他で指摘のあった「ツリー(Tree)ではない」ですが、DocumentFragmentに寄せた構造と感じましたので、class DFとしています。 その意味では、createTreeの関数名は好ましくないですね。 DOMTreeを意識するならば、DOMはルートノードに document を置く構造なので、parent: nullではなく、rootを参照するノードを持ってくれば、ツリー形式を維持できると思います。
Zuishin

2020/09/15 12:08

「こんな再利用性のない作業にクラス化とか API 化とか意味不明」と思っていましたが、素晴らしいエスパー能力です。まさか質問者がこれを待っていたとは。
Zuishin

2020/09/15 13:35 編集

ちなみに id でソートするだけなら datas.sort((a, b) => (a.id - b.id)) でできるので、そのためだけにクラス化する必要はありません。id でソートするだけでは親が必ず先に来るとは限らないというなら、ソートで対応するのは難しいと思うので、私なら queue を使いますね。まあ私の追記のコードは親が先に来る必要は無いのでソートもキューも不要ですが。
Zuishin

2020/09/15 13:04

ところで、ハッシュマップというのはどれのことなんでしょうか? 以下を見る限り、JavaScript の Map はハッシュマップとは限らないようなんですが。 https://tc39.es/ecma262/#sec-map-objects > Map object must be implemented using either hash tables or other mechanisms
think49

2020/09/16 05:15

@Zuishin さん ありがとうございます。修正しました。 ※言い訳になりますが、途中で気が付いて「ハッシュマップ -> マップ」の修正したつもりが、一カ所修正が漏れていました。
think49

2020/09/16 05:29

@manusa360 さん > ハッシュマップ生成処理のアドバイスをいただきまして、とても助かります。 マップにnew Mapではなく、オブジェクト初期化子を利用している点を除けば、 - @Zuishinさんの追記コード - @jun68yktさんのコード は同じ発想で作られています。 --- class化はその先にある発展系なので、ロジック理解の要ではありません。 私はDOM互換APIを期待して書きましたが、要件次第です。 https://gist.github.com/think49/a2a91f34bb947b3b5a841fd8b4d3342a
Zuishin

2020/09/16 05:44

以前も気になったんですが、オブジェクト「初期化子」とオブジェクトを混同していませんか? 確かに私のコードで初期化子は使われていますが、これはそのコードの本質ではなく、初期化子を使わなくても同じものが書けます。 オブジェクト初期化子を使ったもの > .reduce((result, item) => { result[item.id] = item; return result; }, {}); 使わないもの > .reduce((result, item) => { result[item.id] = item; return result; }, new Object()); https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Object_initializer http://www.ecma-international.org/ecma-262/6.0/#sec-object-initializer > An object initializer is an expression describing the initialization of an Object, written in a form resembling a literal. It is a list of zero or more pairs of property keys and associated values, enclosed in curly brackets. The values need not be literals; they are evaluated each time the object initializer is evaluated.
think49

2020/09/16 14:01

@Zuishin さん 親記事に追記しました。 > 以前も気になったんですが、 気になったのは、どのコメントでしょうか。
Zuishin

2020/09/16 14:13 編集

いつだったか忘れましたが、そのようなことがありましたし、やはり今もそう思います。 初期化子は文法上の言葉です。new Object() ではオブジェクト初期化子は使われません。公式を引用したので見ていただければと思います。 Map とオブジェクトを比較するというのがおかしいと思った時に、「オブジェクト初期化子」というすでにある言葉に独自の用法を作るのではなく、Map の何とオブジェクトの何を比較しているのかを整理してみてはどうでしょうか。いくら「自分は使い分けている」と主張しても、独自の使い分けでは意味をなさないと思います。 双方ともに「連想配列として使用した時に」という文脈の中で使われますから、「連想配列としてのオブジェクト」で良さそうな気がします。
think49

2020/09/16 14:21

申し訳有りませんが、論旨を理解できません。 「オブジェクト初期化子」と表現する事にどのような問題があるのでしょうか。
think49

2020/09/16 14:26

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

2020/09/16 14:40

> 双方ともに「連想配列として使用した時に」という文脈の中で使われますから、「連想配列としてのオブジェクト」で良さそうな気がします。 JavaScriptに「連想配列」の定義はなく、別言語の定義を引きずって意図とは別の意味を含んだ解釈をする人がいるので、私は避けています。 「オブジェクト初期化子」はECMAScriptで定義された用語なので、使用しています。 http://www.ecma-international.org/ecma-262/11.0/#sec-object-initializer
think49

2020/09/16 14:56 編集

データの性質の原理に主眼を置くなら、元々そこまで説明する意図はありませんでした。 キーワードを与えて、後はご自身で調べることを期待しました。 - new Map - オブジェクト初期化子 そもそも、原理を説明するなら、new Mapも説明しないと公平ではありません。 - new Map は get, setメソッドで key に対応した value をget/setできる - Object型のデータは「プロパティ名/プロパティ値」がkey/valueに対応しており、プロパティアクセサでget/setできる ただ、それでも「マップの構成原理を説明していなかったことが問題」といえても、「オブジェクト初期化子というキーワードが不適切」には繋がらないので、やはり意図が理解できません。
Zuishin

2020/09/16 14:58

生成されるオブジェクトではなく、文法上の言葉です。if 文が文であるように、{} はオブジェクト初期化子なんです。これはインスタンスを意味するものではありません。
Zuishin

2020/09/16 15:01

そもそもオブジェクトやインスタンスを「initializer」と呼ぶことに何の違和感もありませんか? これは「何かを初期化するもの」という意味です。
Zuishin

2020/09/16 15:06

例えば int a = 0; という C 言語の文法の場合、「= 0」が初期化子です。 それを見た初心者が int 型の数値のことをいつでも初期化子と呼び、「小数を初期化子にキャストして……」と言っていたらおかしくありませんか?
think49

2020/09/16 15:10

@Zuishinさんの追記コードは「オブジェクト初期化子を使用しています」 そこに間違いはありません。 私はnew Mapを使い、@Zuishinさんはオブジェクト初期化子を使いました。 それも間違っていません。
think49

2020/09/16 15:13

私としては、オブジェクト初期化子は {} と等価であり、new Map も {} もparseしなければ何も実行されたコードという意味では何も変わらない認識です。 そこに「文法」と「インスタンス」で区別する理由はない考えです。 どちらも同じ「コード」です。
think49

2020/09/16 15:16

{} も new Object も「Objectのインスタンス」です。 console.log({} instanceof Object); console.log(new Object() instanceof Object);
Zuishin

2020/09/16 15:24

文法とインスタンスは明確に違います。
Zuishin

2020/09/16 15:27

もしインスタンスのことを初期化子と呼んでいいのだとしたら、インスタンスのことを定義と呼んでもよくなりますね。初期化子によって定義されているわけですから。 私は定義を使いました。think49 さんは Map を使いました。なんだこれ?ってなりませんか?
Zuishin

2020/09/16 16:08 編集

> 「オブジェクト初期化子」はECMAScriptで定義された用語なので、使用しています。 > http://www.ecma-international.org/ecma-262/11.0/#sec-object-initializer これ、私が引用したところと同じですが、ちゃんと読みましたか? > An object initializer is an expression describing the initialization of an Object 日本語にしましょうか? 「オブジェクト初期化子はオブジェクトの初期化を記述する式です」 式なんですよ。 if 文が文であるように。 代入式が式であるように。 オブジェクト初期化子は式なんです。 インスタンスのことでもクラスのことでもありません。
think49

2020/09/16 23:33 編集

> もしインスタンスのことを初期化子と呼んでいいのだとしたら、インスタンスのことを定義と呼んでもよくなりますね。初期化子によって定義されているわけですから。 そもそも、私は「オブジェクト初期化子を使用する」にインスタンスの意味を込めていません。 私の一文を深読みされているように見受けられますが、言葉通りに受け取って頂いて構いません。 > マップにnew Mapではなく、オブジェクト初期化子を利用している点を除けば、 「new Mapではなく、{} のコードを書く(利用する)ことで、マップという構造を作り出している点を除けば」と読み替えて下さい。 オブジェクト初期化子は「コードの断片」を示しているに過ぎず、抽象的な概念の意図は一切ありません。
think49

2020/09/16 23:42

> 「オブジェクト初期化子はオブジェクトの初期化を記述する式です」 前述の通り、コードの断片を示す意図でしかなく、式を示すことに問題はないと考えます。 ちなみに、new Mapも式ですが、そちらも不適切な表現なのでしょうか。
Zuishin

2020/09/17 00:13

> マップにnew Mapではなく、オブジェクト初期化子を利用している点を除けば、 > JavaScriptに「連想配列」の定義はなく、別言語の定義を引きずって意図とは別の意味を含んだ解釈をする人がいるので、私は避けています。 「連想配列」ではなく「マップ」なら良いということですか? 「マップ」は Map オブジェクトあるいは Array.prototype.map() のことではないでしょうか。 ここで「マップ」と呼ばれたものは「オブジェクト初期化子」でも「new Map」でもないことは明白です。 > 「オブジェクト初期化子」は文法上の言葉ですが、生成されるオブジェクトは確定しています。 > {} と書いても、new Object と書いても、生成される値は全く変わりません。 > そこに誤解が生じる可能性を私は感じていません。 {} はオブジェクト初期化子ですが、new Object はそうではありません。 そこで生成されるものを問題にしているのならば、やはりインスタンスをオブジェクト初期化子と呼んでいるようにしか思えません。
think49

2020/09/17 05:19

> そこで生成されるものを問題にしているのならば、やはりインスタンスをオブジェクト初期化子と呼んでいるようにしか思えません。 生成されたオブジェクトに言及したのは、@Zuishinさんから指摘があったことで「@Zuishinさんに返答」しましたが、 > マップにnew Mapではなく、オブジェクト初期化子を利用している点を除けば、 この一文にインスタンスの意図は全くありません。 むしろ、この一文でインスタンスと読み取る方が私は理解に苦しみます。 どう読んでも、この一文ではインスタンスと解釈出来ませんが、どういう論理でインスタンスと読めるのでしょう? 私が行ったのは、new Mapと{}を比較出来るようにヒントを与えただけで、それ以降の考察は質問者自身がやるべき事です。 元々の質問では「マップ」に言及してませんから、これは回答ではありません。 真面目に回答するならば、「質問者が理解してないこと」「質問者が調査したこと」を明らかにした上で回答を行うべきです。 質問者の理解度を勝手に考察して「ここまで説明すべき」とするのはエスパーを期待しすぎと考えます。
Zuishin

2020/09/17 05:40

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

0

こんにちは

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

javascript

1const [tree] = datas.reduce(([ary, map], e) => { 2 const node = { id: e.id, children: [] }; 3 if (e.parent === null) { 4 ary.push(node); 5 } else if (map[e.parent]) { 6 map[e.parent].children.push(node); 7 } 8 map[e.id] = node; 9 return [ary, map]; 10}, [[], []]);

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

参考になれば幸いです。

追記

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

javascript

1const findTreeNodeById = (tree, id) => { 2 for (let i = 0; i < tree.length; ++ i) { 3 if (tree[i].id === id) { 4 return tree[i]; 5 } else if (tree[i].children.length > 0) { 6 const node = findTreeNodeById(tree[i].children, id); 7 if (node) { 8 return node; 9 } 10 } 11 } 12 return null; 13}

追記2

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

var tree = [{
id: 0,

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

投稿2020/09/13 10:44

編集2020/09/13 15:38
jun68ykt

総合スコア9058

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

manusa360

2020/09/15 11:00

ご回答ありがとうございます。 nodeの上から探して行って、そこの層に見つからなければ、深い層を探していく・・・というやり方ですね。 ただツリーが複数あるような場合では、あるツリーを深くまで探して行って結局見つからなくなった場合に、浅い層まで戻って別の系統を深くまで探していく、、、というやり方ができないため、ツリー内をくまなく探せるアルゴリズムを探索しています。 ※追記2でおっしゃられるように、datasにはparentがnullの要素を複数入れますので、最終的にツリーを格納する変数は「trees」にします。
jun68ykt

2020/09/16 09:46

@manusa360さん > ただツリーが複数あるような場合では、 > ・・・ > ツリー内をくまなく探せるアルゴリズムを探索しています。 なるほどです。そのお話ですと、単純な再帰では解決できないような、ちょっと込み入った課題に挑戦されているようですね。うまく問題解決できることを祈念申し上げます。
jun68ykt

2020/09/17 15:18 編集

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

0

再帰要りませんでした。

JavaScript

1var datas = [ 2 { 3 id: 0, 4 parent: null 5 }, 6 { 7 id: 1, 8 parent:0 9 }, 10 { 11 id: 2, 12 parent:0 13 }, 14 { 15 id: 3, 16 parent:1 17 }, 18 { 19 id: 4, 20 parent: 1 21 }, 22 { 23 id: 5, 24 parent: 2 25 } 26 ]; 27 28const result = datas.map(a => { return {"id":a.id, "children":[]}; }); 29for (const datum of datas.filter(a => a.parent !== null)) { 30 result[datum.parent].children.push(result[datum.id]); 31} 32const tree = [result[0]]; 33console.log(JSON.stringify(tree, undefined, 2));

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

JavaScript

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

投稿2020/09/13 10:35

編集2020/09/13 11:55
Zuishin

総合スコア28669

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

think49

2020/09/15 11:45

> 追記 id が連番でない、またはルートが複数ある場合 今更ですが、「ルートが複数ある」は違和感のある表現ですね。 それはもうルートではないのでは…。
Zuishin

2020/09/15 11:57 編集

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

2020/09/15 12:09

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

0

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

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

javascript

1class Node { 2 3 constructor ( id ) { 4 this.parent = null 5 this.id = id; 6 this.children = []; 7 } 8 hasChildNodes () { 9 return !!this.children.length; 10 } 11 appendChild( node ) { 12 if( Node.isNode(node) ) { 13 node.parent = this; // parent 14 this.children.push( node ); 15 } 16 } 17 removeChild( node ) { 18 if( this.parent && Node.isNode(node) ) { 19 let idx = this.children.indexOf( node ); 20 if( ~idx ) { 21 this.children.splice( idx, 1 ); 22 node.parent = null; // parent 23 } 24 } 25 } 26 getNodeById ( id ) { 27 if( this.id === id ) return this; 28 let rslt; 29 if( this.hasChildNodes() ) { 30 this.children.some( child => (rslt = child).id === id ); 31 } 32 return rslt; 33 } 34 35 toJson () { 36 let 37 { id, children } = this, 38 childNodes = [] 39 ; 40 if( this.hasChildNodes() ) { 41 children.forEach( child => childNodes.push( child.toJson() ) ); 42 children = childNodes; 43 } 44 return { id, children }; 45 } 46 47 static isNode( node ) { 48 return node instanceof Node; 49 } 50 51 static fromDatas ( datas ) { 52 let rootNode; 53 for ( let {id, parent} of datas ) { 54 let 55 node = new Node( id ), 56 parentNode = null 57 ; 58 if( !rootNode ) { 59 rootNode = new Node(id); 60 } 61 else if( parent!==null ) { 62 parentNode = rootNode.getNodeById( parent ); 63 parentNode.appendChild( node ); 64 } 65 } 66 return rootNode; 67 } 68} 69 70 71 72var datas = [ 73 { id: 0, parent: null }, 74 { id: 1, parent: 0 }, 75 { id: 2, parent: 0 }, 76 { id: 3, parent: 1 }, 77 { id: 4, parent: 1 }, 78 { id: 5, parent: 2 } 79]; 80var tree1 = Node.fromDatas(datas).toJson(); 81 82console.log( tree1 ); // { id, parent, children } : ルートノード 83 84/* 850①--1②--3③--5④ 86  |  |  |-6④ 87  |  |-4③--7⑤ 88  |    |-8⑤ 89  |-2②--9⑥ 90 */ 91var datas2 = [ 92 { id: 0, parent: null }, 93 { id: 1, parent: 0 }, 94 { id: 2, parent: 0 }, 95 { id: 3, parent: 1 }, 96 { id: 4, parent: 1 }, 97 { id: 5, parent: 3 }, 98 { id: 6, parent: 3 }, 99 { id: 7, parent: 4 }, 100 { id: 8, parent: 4 }, 101 { id: 9, parent: 2 } 102]; 103var tree2 = Node.fromDatas(datas2); 104console.log( tree2 ); // { id, parent, children } : ルートノード 105 106var tree = [ tree2.toJson() ]; // 探索用に collection ライクにする。 107console.log( tree ); 108console.log( tree[0].children ); 109console.log( tree[0].children[0].children ); 110console.log( tree[0].children[0].children[0].children ); 111console.log( tree[0].children[0].children[1].children ); 112console.log( tree[0].children[1] ); 113 114console.log( JSON.stringify( tree, null, 2) )

追記)

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

投稿2020/09/16 10:32

編集2020/09/16 10:52
AkitoshiManabe

総合スコア5434

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

think49

2020/09/16 13:23

> DOM ライクな実装は「ルートが1つ」に制限されますが、「文書断片を意味するノードをルートとする」ことを考えれば、その断片がもつ children を tree の Collection とするアイディアになります。 私は DOM Interface の NodeList, HTMLCollection, DocumentFragmentのInterface模倣が終着点と考えました。 私の回答コードでは DocumentFragment を参考にしました。 appendChildは可変長引数に変更しましたが…。
AkitoshiManabe

2020/09/16 22:24

私の回答では 様々に派生するNode を参考にしています。 DOM lv2が実用されていた頃に JsonML element で DOMを偽装しようと試みたことがあるのですが、「ノードを抽象化してツリーを構成する場合、親子関係が重要」であるとの経験則が私の回答になります。 JsonML では Document、DocumentFragment、NodeList、HTMLCollection はどれも tagName を "" として、内容の異なる子要素の構成で使い分け(仕様とは全く異なる扱い方)をしなければ、使いづらかった。 「仕様はあるのだけど、非公開の個人利用なら問題ないよね」という大雑把な考え方です…。 (nodeType も無理やり増やしたりしていました)
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

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

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問