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

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

ただいまの
回答率

90.32%

  • JavaScript

    17549questions

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

  • for

    257questions

    for文は、様々なプログラミング言語で使われている制御構造です。for文に定義している条件から外れるまで、for文内の命令文を繰り返し実行します。

  • ハッシュ

    41questions

    ハッシュは、高速にデータ検索を行うアルゴリズムのことです。

【JavaScript】ネストされた連想配列の中で、マッチするプロパティにたどり着くまでの、プロパティの一覧を取得したいです

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 3,146

kde

score 19

前提・実現したいこと

JavaScriptで、ネストされた連想配列の中でマッチする任意のプロパティ(ID)にたどり着くまでの親プロパティ(ID)を配列として取得したいです。

下記のデータの中から、例えば"100106"と引数に指定したら、["1", "10", "1001", "100106"]と返ってくる関数を作れればと考えています。
"15"であれば["1", "15"]
"2101"であれば["2", "21", "2101"]
といった具合です。

マッチしなかったらfalseやnullが返ってきてほしいです。

for in文などを組み合わせて試みたものの、上手くいっていません。。
ご教示いただけますと幸いです。

var obj = {
  "1": { "name": '', "child": {
    "10": { "name": '', "child": {
      "1001": { "name": '', "child": {
        "100101": { "name": '', "child": {}},
        "100106": { "name": '', "child": {}},
      }},
      "1002": { "name": '', "child": {
        "100201": { "name": '', "child": {}}
      }}
    }},
    "15": { "name": '', "child": {
      "2001": { "name": '', "child": {}}
    }},
    "20": { "name": '', "child": {
      "2006": { "name": '', "child": {}}
    }}
  }},
  "2": { "name": '', "child": {
    "21": { "name": '', "child": {
      "2101": { "name": '', "child": {
        "10210101": { "name": '', "child": {}}
      }},
      "2102": { "name": '', "child": {}}
    }}
  }}
};

データについては、下記の形を基本としており、childプロパティの中にネストされています。

"id(数値)": {
  name: '',
  child: {/* 子がいれば入る */}
}

補足情報

ES5でもES2015でも問題ありません。

 試したコード

下記が途中まで試してみたコードです。
とりあえずどうにか取れるのを確認できればと思って実装しました。
第一階層の直下配下に属するものであれば問題ないのですが、その他だと上手く取れていないようです。

<考えていたロジック>

  • パースしたidを配列に入れていく
  • パースしていく中で指定のidとマッチしたらループ終了
  • childプロパティが空だったら(= 指定のIDに当たらなければ)配列を空にして次のループを実行する

<分からないこと>

  • ロジックが誤っていると思うが、どういったロジックが良いかわからない。考え中。
  • ループの途中で何かしら処理を加える必要があると思うが、ロジックが定まっていないためどのような処理を加えればいいか分からない
console.log(getPropPath('1001'));     // => ["1", "10", "1001"] OK
console.log(getPropPath('100106'));   // => ["100106"] NG
console.log(getPropPath('100201'));   // => ["1002", "100201"] NG
console.log(getPropPath('1003'));     // => ["1003"] NG
console.log(getPropPath('10210101')); // => ["2", "21", "2101", "10210101"] OK

function getPropPath(id) {
  var result = [];

  HOGE:
  for (var key in obj) {
    result.push(key);
    if (key === id) { break HOGE; }
    if (Object.keys(obj[key].child).length === 0) { 
      result.length = 0;
      continue;
    }
    var obj1 = obj[key].child;
    for (var key in obj1) {
      result.push(key);
      if (key === id) { break HOGE; }
      if (Object.keys(obj1[key].child).length === 0) { 
        result.length = 0;
        continue;
      }
      var obj2 = obj1[key].child;
      for (var key in obj2) {
        result.push(key);
        if (key === id) { break HOGE; }
        if (Object.keys(obj2[key].child).length === 0) { 
          result.length = 0;
          continue;
        }
        var obj3 = obj2[key].child;
        for (var key in obj3) {
          result.push(key);
          if (key === id) { break HOGE; }
          if (Object.keys(obj3[key].child).length === 0) { 
            result.length = 0;
            continue;
          }
          var obj4 = obj3[key].child;
          for (var key in obj4) {
            result.push(key);
            if (key === id) { break HOGE; }
            if (Object.keys(obj4[key].child).length === 0) { 
              result.length = 0;
              continue;
            }
          }
        }
      }
    }
  }

  return result;
}
  • 気になる質問をクリップする

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

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

  • kei344

    2017/12/09 14:44

    ご自身で試されたコードを質問文に追記し、「何」が「どのように」わからないのか、コードのどの部分で詰まっているのかなどを具体的に追記されたほうが回答が望めると思います。

    キャンセル

  • kde

    2017/12/09 15:44

    コメントありがとうございます。試したコードや不明点を書いてみましたが、そもそもロジックが定まっていないのが一番よくないところに思えました。

    キャンセル

回答 1

checkベストアンサー

+3

for in文などを組み合わせて試みたものの、上手くいっていません。。

プログラミングスキルでゴリ押しで解くことは可能です、
使うなら最も楽なのが再帰関数を用意することです。
例えばこんな感じ。

var obj = {
  "1": { "name": '', "child": {
    "10": { "name": '', "child": {
      "1001": { "name": '', "child": {
        "100101": { "name": '', "child": {}},
        "100106": { "name": '', "child": {}},
      }},
      "1002": { "name": '', "child": {
        "100201": { "name": '', "child": {}}
      }}
    }},
    "15": { "name": '', "child": {
      "2001": { "name": '', "child": {}}
    }},
    "20": { "name": '', "child": {
      "2006": { "name": '', "child": {}}
    }}
  }},
  "2": { "name": '', "child": {
    "21": { "name": '', "child": {
      "2101": { "name": '', "child": {
        "10210101": { "name": '', "child": {}}
      }},
      "1002": { "name": '', "child": {}}
    }}
  }}
};
var parse = (obj, path = []) => {
  const keys = Object.keys(obj);
  if (keys.length === 0) return []; // 既定部
  return keys
    .map(key => [[...path, key], ...parse(obj[key].child, [...path, key])])
    .reduce((arr, it) => [...arr, ...it], []);
}
var pathByKey = parse(obj).reduce((path, it) => { path[it[it.length - 1]] = it; return path}, {});
console.log(pathByKey);
{ '1': [ '1' ],
  '2': [ '2' ],
  '10': [ '1', '10' ],
  '15': [ '1', '15' ],
  '20': [ '1', '20' ],
  '21': [ '2', '21' ],
  '1001': [ '1', '10', '1001' ],
  '1002': [ '2', '21', '1002' ],
  '2001': [ '1', '15', '2001' ],
  '2006': [ '1', '20', '2006' ],
  '2101': [ '2', '21', '2101' ],
  '100101': [ '1', '10', '1001', '100101' ],
  '100106': [ '1', '10', '1001', '100106' ],
  '100201': [ '1', '10', '1002', '100201' ],
  '10210101': [ '2', '21', '2101', '10210101' ] }

15キーの配下に2001が入っていたりしますが、これは探索木ですか?
もし探索木のつもりなら要件を満たしていません。

結局全て開いてみる必要がありこれが正解であれば、
私の回答のようにに全て探索してインデックスを作っておくのが最善になるでしょう。

もし機密情報だったりして他人に理由を説明出来ないのであれば仕方ないですが、
データ部をもう少しマシな設計に出来ないか検討してみてください。


【追記】

マッチしなかったらfalseやnullが返ってきてほしいです。 

上記のコードから進めます。

var pathByKey = parse(obj).reduce((path, it) => { path[it[it.length - 1]] = it; return path}, {});
console.log(pathByKey['1001']);     // [ '1', '10', '1001' ]
console.log(pathByKey['100106']);   // [ '1', '10', '1001', '100106' ]
console.log(pathByKey['100201']);   // [ '1', '10', '1002', '100201' ]
console.log(pathByKey['1003']);     // undefined
console.log(pathByKey['10210101']); // [ '2', '21', '2101', '10210101' ]

// falseやnullが返ってきて欲しいです。
console.log(pathByKey['1003'] || false); // false
console.log(pathByKey['1003'] || null);  // null

JavaScriptでは左辺がFalsy(if文に単体で突っ込んでfalseと判定される値、0とかnullとか)の場合、
a || bと記載しておけば右辺を取得出来ます。


【おまけ】

コメント読みました、なるほど…確かに長く業務が動いていたりするとどっかにしわ寄せが行くケースがあります。
わかりました。勉強と実践で役立つ様に最終型も用意しておきました。
最初からこの形式になってくれれば、めっちゃ使いやすいんじゃないですか?

{ '1': { key: '1', name: '', path: [ '1' ], children: [ '1', '2' ] },
  '2': { key: '2', name: '', path: [ '2' ], children: [ '1', '2' ] },
  '10': 
   { key: '10',
     name: '',
     path: [ '1', '10' ],
     children: [ '10', '15', '20' ] },
  '15': 
   { key: '15',
     name: '',
     path: [ '1', '15' ],
     children: [ '10', '15', '20' ] },
  '20': 
   { key: '20',
     name: '',
     path: [ '1', '20' ],
     children: [ '10', '15', '20' ] },
  '21': { key: '21', name: '', path: [ '2', '21' ], children: [ '21' ] },
  '1001': 
   { key: '1001',
     name: '',
     path: [ '1', '10', '1001' ],
     children: [ '1001', '1002' ] },
  '1002': 
   { key: '1002',
     name: '',
     path: [ '2', '21', '1002' ],
     children: [ '1002', '2101' ] },
  '2001': 
   { key: '2001',
     name: '',
     path: [ '1', '15', '2001' ],
     children: [ '2001' ] },
  '2006': 
   { key: '2006',
     name: '',
     path: [ '1', '20', '2006' ],
     children: [ '2006' ] },
  '2101': 
   { key: '2101',
     name: '',
     path: [ '2', '21', '2101' ],
     children: [ '1002', '2101' ] },
  '100101': 
   { key: '100101',
     name: '',
     path: [ '1', '10', '1001', '100101' ],
     children: [ '100101', '100106' ] },
  '100106': 
   { key: '100106',
     name: '',
     path: [ '1', '10', '1001', '100106' ],
     children: [ '100101', '100106' ] },
  '100201': 
   { key: '100201',
     name: '',
     path: [ '1', '10', '1002', '100201' ],
     children: [ '100201' ] },
  '10210101': 
   { key: '10210101',
     name: '',
     path: [ '2', '21', '2101', '10210101' ],
     children: [ '10210101' ] } }
> console.log(pathByKey['1001'])
{ key: '1001',
  name: '',
  path: [ '1', '10', '1001' ],
  children: [ '1001', '1002' ] }

> console.log(pathByKey['100106'])
{ key: '100106',
  name: '',
  path: [ '1', '10', '1001', '100106' ],
  children: [ '100101', '100106' ] }

> console.log(pathByKey['100201'])
{ key: '100201',
  name: '',
  path: [ '1', '10', '1002', '100201' ],
  children: [ '100201' ] }

> console.log(pathByKey['1003'])
undefined

> console.log(pathByKey['10210101'])
{ key: '10210101',
  name: '',
  path: [ '2', '21', '2101', '10210101' ],
  children: [ '10210101' ] }

対応するコードがこれです。
おまけではこれを基準に解説します。
(多少読みやすくする為にリファクタリングも行っています)

var obj = {
  "1": { "name": '', "child": {
    "10": { "name": '', "child": {
      "1001": { "name": '', "child": {
        "100101": { "name": '', "child": {}},
        "100106": { "name": '', "child": {}},
      }},
      "1002": { "name": '', "child": {
        "100201": { "name": '', "child": {}}
      }}
    }},
    "15": { "name": '', "child": {
      "2001": { "name": '', "child": {}}
    }},
    "20": { "name": '', "child": {
      "2006": { "name": '', "child": {}}
    }}
  }},
  "2": { "name": '', "child": {
    "21": { "name": '', "child": {
      "2101": { "name": '', "child": {
        "10210101": { "name": '', "child": {}}
      }},
      "1002": { "name": '', "child": {}}
    }}
  }}
};
var parse = (obj, path = []) =>
  Object
    .keys(obj)
    .map(key => [
      {
        key: key,
        name: obj[key].name,
        path: [...path, key],
        children: Object.keys(obj)
      },
      ...parse(obj[key].child, [...path, key])
    ])
    .reduce((arr, it) => [...arr, ...it], []);
var pathByKey = parse(obj).reduce((o, it) => {
  o[it.key] = it;
  return o;
}, {});

今回利用したビルトイン関数は下記です。

a => a + 1

この書き方はES2015で実装されたアロー関数です。
例のa => a + 1function (a) { return a + 1; }です。
(ただし、thisのスコープを新たに作らなかったり、aruguments変数を用意しなかったりと簡易的な関数として振る舞いますので、これが原因でハマる箇所もあります、ちらっと覚えておいてください)

[...path, key]

この書き方もES2015で追加された書き方です。
配列[]やオブジェクト{}の宣言時に頭に...を追加すると、展開してから追加の合図になります。

path.push('123')に似ていますね。
しかし、path.pushは破壊的なメソッドですので、
path.push('123')とするとpathという変数自身に改変が加わってしまいます。
このため、毎回新しくコピーを作り直すという目的でpath.pushの代わりに利用しています。

Object.keys

便利ですね。これはES5の機能なのでガンガン使いましょう。
JSに於けるfor...inはprototypeも取ってくるので、
(アプリを作りたい一般エンジニアにとって)はゴミ以下の価値しかありません。
今回は後述のリスト操作系のメソッドを利用したかったので採用しています。

Array.prototype.map

難解かつ記述量が一気に減るJS使いの必殺技その1です。
これは配列全ての要素に同じ関数を適用し、戻り値を元に配列を再構成するという機能になります。

状態変数を作らずに記述出来るので、上手く使えば行数が減ったり簡素な書き方になることが期待出来ます。

Array.prototype.reduce

難解かつ記述量が一気に減るJS使いの必殺技その2です。
第二引数の値を出発点として、配列の要素数同じ関数を実行して、戻り値を雪だるまのように転がしながら結果を得る手法です。
「畳み込み」と呼ばれる手法ですね。

var add = (a, b) => a + b;
var sum = arr => arr.reduce(add, 0);
console.log(sum([1, 2, 3, 4]));
// 10
  • 1回目: 0(第二引数の初期値) + 1 -> 1
  • 2回目: 1 + 2 -> 3
  • 3回目: 3 + 3 -> 6
  • 4回目: 6 + 4 -> 10

このような計算を裏で行った結果になります。
add関数を1行にすればsum = arr => arr.reduce((a, b) => a + b, 0)と1行で簡単に宣言出来ますね。
今回は2箇所でこのreduceを使っており、慣れるまでは難解なので解説します。

1個目の使い方はobj.child.child.child...と掘っていくと、
戻り値がどんどんネストしていってしまいます。
イメージとしてはこんな感じ。

var item = {key: "1010", path ["1", "10", "1010"]}; // objにアクセスするとこんな感じのデータが取得出来る
var children = [item, [item, [item, [item]]]]; // 再帰関数でどんどん掘っていくとこうなってしまう

これではなんにも使えないですよね。
そこで、毎回第二引数以降のアイテムを一次元配列に引き戻しています。
下記のようなイメージになります。

var item = {key: "1010", path ["1", "10", "1010"]}; // objにアクセスするとこんな感じのデータが取得出来る
var children = [[item], [item, item]; // ああ、ネストしてしまったやばい
var new_children = children.reduce((arr, it) => [...arr, ...it], []); // [item, item, item]

2個目の使い方は空のオブジェクトを宣言して、
1次元配列のアイテムを次々と宣言してくっつけていっています。
こっちは上記の解説の応用ですんなり理解出来そうなので割愛します。

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2017/12/10 12:50

    ご返信遅くなり申し訳ございません。
    すごい…!こんなにきれいに書けてしかも結果として扱いやすい形になっていて、想像以上のご回答を頂けて大変ありがたく思います。

    > 15キーの配下に2001が入っていたりしますが、これは探索木ですか?
    これは探索木ではないです。
    おっしゃる通り、データ部の設計がおかしくなっているのですが、すでに運用されているデータのためこちらでは手を入れることができず、フロント側でなんとかせざるを得ないという状況です。。
    なので、確かに一度すべて開いてみるのが正解だと思いました。

    頂いた処理内容のロジックを、今の自分のJS力では理解できていないのですが、じっくりと紐解かせて頂きたいと思います。
    このたびは助けて頂き誠にありがとうございます。

    キャンセル

  • 2017/12/10 16:53

    あのコードは流石にデモンストレーション程度のものだったので、
    実践用に多少読みやすく、改良加えて、解説もセットで載せました。
    ご参考にどうぞ。

    キャンセル

  • 2017/12/11 00:59

    おおお…まさかここまで丁寧に説明して頂けるなんて…。感激しました…!
    >最初からこの形式になってくれれば、めっちゃ使いやすいんじゃないですか?
    間違いなく使いやすいですね。まさかこんな形式に加工できるなんて思ってもみなかったです。
    Array.prototype.mapとArray.prototype.reduceはほぼ使ったことがなく、特にreduceに関してはまだちゃんと理解が追い付いていないのですが、この必殺技を身に付けることで、複雑なデータ操作がスマートに行えそうですね。
    教えて頂いたこの内容を反芻して試しながらちゃんと使いこなせるように理解したいと思います。
    本当にありがとうございます。

    キャンセル

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

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

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

  • JavaScript

    17549questions

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

  • for

    257questions

    for文は、様々なプログラミング言語で使われている制御構造です。for文に定義している条件から外れるまで、for文内の命令文を繰り返し実行します。

  • ハッシュ

    41questions

    ハッシュは、高速にデータ検索を行うアルゴリズムのことです。

  • トップ
  • JavaScriptに関する質問
  • 【JavaScript】ネストされた連想配列の中で、マッチするプロパティにたどり着くまでの、プロパティの一覧を取得したいです