owned mediaウェブ制作に役立つコンテンツを発信中!

JavaScriptで再帰関数を使ってネストした配列の深さを調べる

配列の値は数値や文字列といったプリミティブ型のものだけではなく、配列やオブジェクトなどが値として含まれることもあります。そういった場合には配列の中に配列があるという、いわゆるネスト(入れ子)の状態になり、こういった配列は多次元配列と呼ばれます。これらネスト構造になった配列について、配列の長さを求めるのはArray.lengthなどを使って簡単に求められますが、深さを求めるのは一筋縄ではいきません。そこで「再帰関数」を使って配列の最大の深さを求めていきます。今回は実際に再帰関数を使ってネスト構造の配列データの最大深度を調べてみたいと思います。    
再帰関数とは?
まず、再帰関数というのはどういうものかを理解しておく必要があります。再帰関数とは、自身の関数の処理中で自身の関数を呼び出すものになります。したがって関数の中で関数を実行することになり、無限ループが起きる可能性があるので、必ず条件付きで関数を呼び出したり、条件に合わない場合には処理を終了させるような形にする必要があります。今回は、再帰的に配列の深さを調べていくことでその深度を求めるために使われます。   では、具体的な方法を検討してみます。今回は配列の中に配列があるという前提でデータを見ていくため、「Array.flat()」のメソッドを使って1階層ずつ配列を1次元配列になるまで平坦化(参考記事「JavaScriptで配列を扱う時によく使うメソッド#10【配列を文字列にする・多次元配列をフラットにする】」)していき、その回数を調べることで配列の深さを求めるという方法で進めてみます。1階層フラットにした配列を次の再帰処理でさらに1階層フラットにするという要領です。実際にコードを見ていきます。
const getDepth = (data) => {
  let depth = 0;
  const recursion = (argument) => {
    if(Array.isArray(argument)) {
      depth++;
      const array = argument.filter((value) => Array.isArray(value));
      if(array.length !== 0) {
        return recursion(array.flat());
      }
    }
  }
  recursion(data);
  return depth;
}
  まず、現在の深さ、つまり再帰処理の回数を記録しておく変数を用意します。そして再帰処理の関数を作成し、引数には検査対象となる配列(もしくはプリミティブ型の値)が入るようにしておきます。再帰処理の関数内では、受け取った引数が配列かどうかをチェックするため、「Array.isArray()」メソッドを使います。引数が配列の場合にネストの深さの変数をインクリメントして現時点の深さを記録していきます。そして「Array.filter()」メソッドを使って、現在の配列から配列型の値、つまり入れ子となっている配列だけを抽出します。(参考記事「JavaScriptで配列を扱う時によく使うメソッド#4【配列から条件に合う値を取得・フィルタリングする】」)   そして、その抽出した配列に対して「Array.flat()」を実行し、1階層平坦化します。この平坦化した値を引数として再帰処理の関数を実行します。その際には値として返すようにします。こうすることで配列が1階層ずつ平坦化される処理が出来上がります。その際には空配列かどうかをチェックする分岐を用意しておくこともポイントです。なぜなら、Array.flat()を空配列に対して実行しても同じ空配列が返されるため無限ループになるからですね。最後に処理の終了時で現在の深さの値を返すようにしておきます。   これで配列の深さを調べる処理ができました。実際に下記のようにダミーのデータを用意して実行すると、その配列の最大の深さが値となって返されるのが確認できます。
const array = [ 1, [ 2, [ 3, 3, [ 4 ], [ 4, [ 5, [ 6 ], 5 ], 4, [ 5 ] ] ], 2 ] ];
getDepth(array);   // -> 6
  再帰関数については慣れないとどういう挙動をするのかイメージしにくい部分もあるかもしれません。そんな場合に、上記コードで言うと再起処理で使われる引数をコンソールで確認することで、どのように平坦化されているのがわかりやすく調べることができます。
......
const recursion = (argument) => {
  console.log({ depth, argument })
......
  実際にコンソール上で出力されたものを見ると、1階層ずつの平坦化が配列内に配列型のデータがなくなるまで続いて、深さの値がカウントアップしているのがわかります。多次元配列だったものが最終的には1次元配列となり、再起処理が終了します。
{ depth: 0, argument: [ 1, [ 2, [ 3, 3, [ 4 ], [ 4, [ 5, [ 6 ], 5 ], 4, [ 5 ] ] ], 2 ] ] }
{ depth: 1, argument: [ 2, [ 3, 3, [ 4 ], [ 4, [ 5, [ 6 ], 5 ], 4, [ 5 ] ] ], 2 ] }
{ depth: 2, argument: [ 3, 3, [ 4 ], [ 4, [ 5, [ 6 ], 5 ], 4, [ 5 ] ] ] }
{ depth: 3, argument: [ 4, 4, [ 5, [ 6 ], 5 ], 4, [ 5 ] ] }
{ depth: 4, argument: [ 5, [ 6 ], 5, 5 ] }
{ depth: 5, argument: [ [ 6 ] ] }
  配列に限定するとですが、このように複雑な入れ子構造になっていても最大深度が求められるようになりました。    
ネストの深さごとに配列の値をソートして深さに対応した入れ子のHTMLに変換
このような段階的に配列を平坦化させていくことで、配列の値を深さ別にソートすることができます。それを応用してHTMLでネスト構造を作成してみます。再帰処理など段階的に平坦化させていく部分など大まかには同じですが、HTML文字列に変換させる処理が加わってきます。
const convertNestElements = (data) => {
  let depth = 0;
  const elements = [];
  const recursion = (argument) => {
    if(Array.isArray(argument)) {
      depth++;
      const array = argument.filter((value) => Array.isArray(value));
      const listItemValues = argument.filter((value) => !Array.isArray(value));
      const listItemElements = listItemValues.map((value) => `<span>${value}</span>`)
      elements.push(
        [
          `<div id="depth_${depth}">`, 
          ...listItemElements
        ]
      );
      if(array.length !== 0) {
         return recursion(array.flat());
      }
    }
  }
  recursion(data);
  for(let index = 0; index < depth; index++) {
    elements.push('</div>');
  }
  document.querySelector("#nest_element").insertAdjacentHTML('beforeend', elements.flat().join(''));
}
const array = [ 1, [ 2, [ 3, 3, [ 4 ], [ 4, [ 5, [ 6 ], 5 ], 4, [ 5 ] ] ], 2 ] ];
convertNestElements(array);
  JavaScriptでまとめてHTMLを挿入する形になるので、配列の中に含まれているHTML文字列の値は全てまとめて文字列に変換しておくとラクですね。この処理で出力されるHTMLは下記のようになります。
<div id="nest_element">
  <div id="depth_1">
    <span>1</span>
    <div id="depth_2">
      <span>2</span>
      <span>2</span>
      <div id="depth_3">
        <span>3</span>
        <span>3</span>
        <div id="depth_4">
          <span>4</span>
          <span>4</span>
          <span>4</span>
          <div id="depth_5">
            <span>5</span>
            <span>5</span>
            <span>5</span>
            <div id="depth_6">
              <span>6</span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
 
  今回は再帰処理を使って配列の深さを調べる方法についてまとめてみました。配列の長さを調べることは簡単ですが、深さを調べる場合にはいろんなパターンの配列のネスト構造を想定しないといけないので少し工夫が必要になってきます。使い所としてはあまりありませんが、ネスト構造が可変のデータを扱う際には使えそうですね。
  • はてなブックマーク
  • Pocket
  • Linkedin
  • Feedly

この記事を書いた人

Twitter

sponserd

    keyword search

    recent posts

    • Twitter
    • Github
    contact usscroll to top
      • Facebook
      • Twitter
      • Github
      • Instagram