配列・連想配列

【JavaScript】配列内容を比較して同一判定および差分を得る方法

更新日:2024/02/27

JavaScriptで配列を比較演算子で比較すると、同じインスタンスを共用しているかどうかを判定します。
そのため内容が同じかどうかの比較ができません。
また内容比較用の関数が用意されていません。

そこで、JavaScriptで二つの配列の内容が同じかを判定する関数と、内容の差分を取得する関数を作成したので紹介します。

 

===での配列比較

配列の内容が同じかどうかを比較する場合、比較演算子(===または==)を使用できません。
比較演算子は内容ではなくて、同じインスタンスを共有(参照)しているかどうかを判定します。

const array1 = [1,2,3];
const array2 = [1,2,3];
  // 内容が同じ配列を比較
console.log( array1 === array2 );
>> false; // 異なる値と判定された

const array3 = array1;
array3[0] = 100;
console.log( array[0] );
>> 100  // array1 と array3は同じ配列を共有

console.log( array1 === array3 );
>> true; // 同じ値と判定された

 

値の順番と内容の同一チェック

配列内の要素の順番(インデックス)と内容が一致するかどうかを判定する方法を二つ紹介します。

JSON.stringify()版

二つの配列のインデックスと内容が同じかどうかの判定は、JSON.stringify()で文字列化してものを比較する処理が一番簡単です。

今回は次のような関数を作成しました。

JSON.stringify()版

// 二つの配列の内容が同じかどうかの判定関数
const isSameArray = (array1,array2)=>{
  if( !Array.isArray(array1) || !Array.isArray(array2)
        || array1.length !== array2.length ) return false;
  
  if( array1.length === 0 ) return true;

  return JSON.stringify(array1) === JSON.stringify(array2);
}

  // 作成した関数の動作確認
console.log( isSameArray( [1,2,3] , [1,2,3] ) );
>> true
console.log( isSameArray( [1,2,3] , [1,2] ) );
>> false

  // 多次元配列で確認
console.log( isSameArray( [1,2,[3,[4]]], [1,2,[3,[4]]]) );
>> true
console.log( isSameArray( [1,2,[3,[4]]] , [1,2,[3,4]]) );
>> false

JSON.stringify()は多次元配列も文字列化してくれるので、気にせず比較できます。

文字列に変換して比較とか効率的にどうなの?と以前は思っていましたが、頻繁に呼び出すのでなければ、それほど気にする必要がないと今は思っています。

JSON.stringify()版の問題点

ただし、JSON.stringify()はundefined値をnullとして出力します。
これは値だけでなく、インデックスが存在しないときも適用されます。

そのため、内容が異なる配列比較で同一判定されることがあります。

JSON.stringify()版の問題点

const array = [ , undefined ];
console.log( array );
>> Array [ // ↓ インデックス0が存在しない
        1: undefined
        length:2
     ]
console.log( JSON.stringify( array ) );
>> "[null,null]" ← undefinedがnullになる!

const array1 = [1,2,3];
delete array1[1];

const array2 = [1,null,3];

  // array1とarray2が同じ文字列に変換される
console.log( JSON.stringify( array1 ) );
>> "[1,null,3]"
console.log( JSON.stringify( array2 ) );
>> "[1,null,3]"

配列要素が削除される可能性があったり、nullとundefinedを異なる値として比較するときは、JSON.stringify()を使用できません。

ちなみに、JSON.stringify()はオブジェクトにも対応しています。
しかし文字列プロパティについては作成順に出力されるため、内容が同じでも出力結果が異なる可能性があります。

JSON.stringify()と文字列プロパティ

console.log( JSON.stringify( { a:1 , b:1 }) );
>> '{"a":1,"b":1}'
console.log( JSON.stringify( { b:1 , a:1 }) );
>> '{"b":1,"a":1}'

JSON.stringify()での同一判定は、オブジェクトを含んでいると誤判定の原因になります。

ループ比較版

配列要素が削除される可能性があったり、nullとundefinedを異なる値として比較するときは各値を一つずつ比較します。

ループ比較版

// 二つの配列の内容が同じかどうかの判定関数 その2
const isSameArray = (array1,array2)=>{
  if( !Array.isArray(array1) || !Array.isArray(array2)
        || array1.length !== array2.length ) return false;
  
  const length = array1.length;
  if( length === 0 ) return true;

  for( let count = 0 ; count < length ; count++){
    const a1 = array1[count], a2 = array2[count];

    if( !array1.hasOwnProperty(count) ){ // インデックスが存在しない
      if( array2.hasOwnProperty(count) ) return false; // 他方にインデックスがある
      continue;
    }
    if( !array2.hasOwnProperty(count) ){ // インデックスが存在しない
      if( array1.hasOwnProperty(count) ) return false; // 他方にインデックスがある
      continue;
    }
      // 両方の値が配列なら再帰
    if( Array.isArray(a1) && Array.isArray(a2) ){
      if( !isSameArray( a1 , a2 ) ) return false;
      continue;
    }
    if( a1 !== a2 ) return false;
  }
  return true;
}

  // 作成した関数の動作確認
console.log( isSameArray( [1,undefined,3], [1,null,3]) ) ;
>> false

  // 多次元配列で確認
console.log( isSameArray( [1,2,[3,[4]]], [1,2,[3,[4]]]) );
>> true

  // インデックス削除配列の確認
const a = [1,2,3], b = [1,2,3];
delete a[1];

console.log( isSameArray( a, b ));
>> false

配列は、deleteなどでインデックスされている可能性があります。
forEach()系のメソッドは存在しないインデックスをスキップするので、for文でループしています。

ループ比較版の問題点

問題点と言うより、注意点ですね。

要素内のオブジェクトは、同じオブジェクトを共有している場合に同値とみなされます。
内容が同じでも、個別に生成されたオブジェクトは同値ではありません。

const obj1 = {value:1};
const obj2 = obj1; // 代入操作は同じオブジェクトを共有
console.log( obj1 === obj2 );
>> true  // obj1とobj2は同値

const obj3 = {value:1}; // 内容が同じオブジェクトを生成
console.log( obj1 === obj3 );
>> false // obj1とobj3は異なる値

また、循環参照に対応していません。
再帰が無限ループしてエラーになることがあります。

const a = [];
a.push(a);
console.log( isSameArray([a],[a] ));
>> RangeError: Maximum call stack size exceeded

 

順番関係なしで内容の同一チェック

二つの配列をインデックスの順番を考慮しないで、内容が同一かどうかチェックします。

方法は、一方の配列(1)をループさせて、もう一方の配列(2)から同一要素を検索します。
同一要素がなければ、配列の内容が不一致として終了します。
同一要素があったら、配列(2)からその要素を削除して配列(1)の次のループを処理します。

配列(1)ループが終了した時点で、配列(2)の要素が0なら配列の内容が一致していると判定できます。

// 配列内容の同一チェック(順番関係なし)
const isSameArrayIgnoreOrder = (array1,array2)=>{
  if( !Array.isArray(array1) || !Array.isArray(array2)
        || array1.length !== array2.length ) return false;
  
  const length = array1.length;
  if( length === 0 ) return true;

    // array2をシャロ―コピー
  const a2 = Object.assign([],array2);

  const result = array1.every( 
      value=>{
        let sameIndex = -1;
        if( Array.isArray(value) ){ // 値が配列
            // a2から同一の配列を検索
          sameIndex = a2.findIndex(
            a2Value =>!Array.isArray(a2Value) ? false
                        : isSameArrayIgnoreOrder( value , a2Value)
          );
        } else sameIndex = a2.indexOf(value);
        
        if( sameIndex < 0 ) return false; // 同値なし⇒チェック終了
        delete a2[sameIndex]; // a2から同値を削除
        return true; // 次の値チェックへ
  });
    // resultがtrueでa2に値が残っていないなら同一の配列
  return result && a2.every( ()=>false );
}

console.log( isSameArrayIgnoreOrder( [1,2,3] , [2,3,1] ) );
>> true
console.log( isSameArrayIgnoreOrder( [1,2,3] , [2,1,2] ) );
>> false
console.log( isSameArrayIgnoreOrder( [3,3,3] , [3,3,3] ) );
>> true
console.log( isSameArrayIgnoreOrder( [undefined,3,1], [1,null,3]) ) ;
>> false
console.log( isSameArrayIgnoreOrder( [1,3,], [1,null,3]) ) ;
>> false

console.log( isSameArrayIgnoreOrder( [1,2,[3,[4]]], [[3,[4]],1,2]) );
>> true
console.log( isSameArrayIgnoreOrder( [1,2,[3,[4]]], [1,[3,4]],2) );
>> false

everyメソッドは、全てのコールバックがtrueを返すと、結果がtrueになります。
コールバックは同値の時にtrueを返しているので、everyの結果がtrueなら、全ての値が同値ということになります。

ただしeveryはforEach系のメソッドなので、配列のインデックスが飛んでいるときコールバック呼び出しがスキップされます。
このときeveryの結果がtrueでも、シャロ―コピーした配列内に値が残っている可能性があります。

例えば次のような二つの配列なら、配列2の要素が一つ残ります。

配列1: [1, ,3] // インデックス1が存在しない
配列2: [1,2,3] // 2が残る

そこで、最後のリターンでシャロ―コピーした配列内に値が残っていないかチェックしています。

なおこのコードは速度改善の余地があります。
シャロ―コピーした配列の要素を配列とそれ以外に分けて、分けた配列要素をfindIndexの対象にします。
そうすることで、ループの回数を減らすことができます。

 

差分を求める

二つの配列の差分を求めるコードを紹介します。
なお、ここで紹介したコードはループ比較版の問題点と同じ問題点があります。

多次元配列と重複を考慮しない場合

最初に、多次元配列と重複を考慮しないときの差分を求める関数コードです。
filterメソッドを使用して差分を求めます。

// array1からarray2の値と一致しない値を配列で取得する
const arrayDiff = (array1,array2)=>{
  if( !Array.isArray(array1) || !Array.isArray(array2) ) return false;
  return array1.filter( value=>array2.indexOf(value) < 0 );
}

console.log( arrayDiff( [1,2,3,4,5] , [2,4] ) );
>> [ 1, 3, 5 ]

次のコードは、差分をインデックスで取得します。
こちらはreduceメソッドを使用します。

// array1からarray2の値と一致しない値のインデックスを配列で取得する 
const arrayDiffIndex = (array1,array2)=>{
  if( !Array.isArray(array1) || !Array.isArray(array2) ) return false;
  return array1.reduce( (result,value,index)=>{
    if( array2.indexOf(value) < 0 ) result.push(index);
    return result; 
  },[]);
}
console.log( arrayDiffIndex( [1,2,3,4,5] , [2,4] ) );
>> [ 0, 2, 4 ]

多次元配列と重複を考慮する場合

多次元配列と重複を考慮する場合はfilterメソッドのコールバック処理に、順番関係なしで内容の同一チェックのeveryメソッドと同じ流れで処理します。

// array1からarray2の値と一致しない値を配列で取得する
// (多次元配列と重複を考慮)
const arrayDiff2 = (array1,array2)=>{
  if( !Array.isArray(array1) || !Array.isArray(array2) ) return false;
  const a2 = Object.assign( [] , array2 );

  return array1.filter( value=>{
        let sameIndex = -1;
        if( Array.isArray(value) ){ // 値が配列
            // a2から同一の配列を検索
          sameIndex = a2.findIndex(
            a2Value =>!Array.isArray(a2Value) ? false
                        : isSameArrayIgnoreOrder( value , a2Value)
          );
        } else sameIndex = a2.indexOf(value);
        
        if( sameIndex < 0 ) return true; // 同値なし ⇒ filter対象
        delete a2[ sameIndex ]; // a2から同値を削除
        return false; // 同値あり  ⇒ filter非対象
  });
}

console.log( arrayDiff2( [1,2,2,3,4,5] , [2,4] ) );
>> [ 1, 2, 3, 5 ]
console.log( arrayDiff2( [1,2,[1,2],[1,2,3]] , [2,4,[1,2]] ) );
>>  [ 1, [ 1, 2, 3 ] ]

isSameArrayIgnoreOrder()は、順番関係なしで内容の同一チェックで紹介している関数です。

差分をインデックスで取得するときは、上記コードをreduceメソッドに置き換えます。

// array1からarray2の値と一致しない値のインデックスを配列で取得する
// (多次元配列と重複を考慮)
const arrayDiff2Index = (array1,array2)=>{
  if( !Array.isArray(array1) || !Array.isArray(array2) ) return false;
  const a2 = Object.assign( [] , array2 );

  return array1.reduce( (result,value,index)=>{
        let sameIndex = -1;
        if( Array.isArray(value) ){ // 値が配列
            // a2から同一の配列を検索
          sameIndex = a2.findIndex(
            a2Value =>!Array.isArray(a2Value) ? false
                        : isSameArrayIgnoreOrder( value , a2Value)
          );
        } else sameIndex = a2.indexOf(value);
        if( sameIndex < 0 ) result.push( index );
        else delete a2[ sameIndex ];

        return result;
  },[]);
}
console.log( arrayDiff2Index( [1,2,2,3,4,5] , [2,4] ) );
>> [ 0, 2, 3, 5 ]
console.log( arrayDiff2Index( [1,2,[1,2],[1,2,3]] , [2,4,[1,2]] ) );
>> [ 0, 3 ]

差分と共通値を取得する

二つの配列の差分と共通値を取得する関数コードです。
結果はオブジェクトで返しています。

// array1とarray2の共通値とそれぞれの差分を取得する
// return: { diff1: array1の差分,common:共通値 , diff2: array2の差分}
const arrayDiff3 = (array1,array2)=>{
  if( !Array.isArray(array1) || !Array.isArray(array2) ) return false;
  const a2 = Object.assign( [] , array2 );

  const result = array1.reduce( (result,value)=>{
        let sameIndex = -1;
        if( Array.isArray(value) ){ // 値が配列
            // a2から同一の配列を検索
          sameIndex = a2.findIndex(
            a2Value =>!Array.isArray(a2Value) ? false
                        : isSameArrayIgnoreOrder( value , a2Value)
          );
        } else sameIndex = a2.indexOf(value);
        
        if( sameIndex < 0 ) result.diff1.push( value );
        else {
          result.common.push( value );
          delete a2[ sameIndex ];
        }
        return result;
  },{diff1:[],common:[]});
  result.diff2 = a2.filter( e=>true );
  return result;
}

console.log( arrayDiff3( [1,2,2,3,4,5] , [2,4] ) );
>> { diff1: [ 1, 2, 3, 5 ], common: [ 2, 4 ] , diff2: []}
console.log( arrayDiff3( [1,2,[1,2],[1,2,3]] , [2,4,[1,2]] ) );
>> { diff1: [ 1, [ 1, 2, 3 ] ], common: [ 2, [ 1, 2 ] ] , diff2: [ 4 ] }

インデックスを返すコードは、次のようになります。
上記コードとほぼ同じです。

// array1とarray2の共通値とそれぞれの差分をインデックスで取得する
// return: { diff1: array1の差分インデックス,common:共通値インデックス(array1基準) 
//   , diff2: array2の差分インデックス}
const arrayDiff3Index = (array1,array2)=>{
  if( !Array.isArray(array1) || !Array.isArray(array2) ) return false;
  const a2 = Object.assign( [] , array2 );

  const result =  array1.reduce( (result,value,index)=>{
        let sameIndex = -1;
        if( Array.isArray(value) ){ // 値が配列
            // a2から同一の配列を検索
          sameIndex = a2.findIndex(
            a2Value =>!Array.isArray(a2Value) ? false
                        : isSameArrayIgnoreOrder( value , a2Value)
          );
        } else sameIndex = a2.indexOf(value);
        
        if( sameIndex < 0 ) result.diff1.push( index );
        else {
          result.common.push( index );
          delete a2[sameIndex];
        }
        return result;
  },{diff1:[],common:[]});
  result.diff2 = a2.map( (e,index)=>index ).filter(e=>true);
  return result;
}

console.log( arrayDiff3Index( [1,2,2,3,4,5] , [2,4] ) );
>> { diff1: [ 0, 2, 3, 5 ], common: [ 1, 4 ], diff2: [] }
console.log( arrayDiff3Index( [1,2,[1,2],[1,2,3]] , [2,4,[1,2]] ) );
>> { diff1: [ 0, 3 ], common: [ 1, 2 ], diff2: [ 1 ] }

更新日:2024/02/27

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。