JavaScriptでのObject比較方法

 他の言語とはひと味違う、JavaScriptのObject比較方法

修正とお詫び
コメントにて、JSON.stringifyがObject順序を保証しない、というご指摘を頂きました。誤った情報を掲載してしまったことをお詫び申し上げます。
JSON.stringify()によると、

注: 配列以外のオブジェクトのプロパティでは、特定の順番で文字列化されることは保証されていません。文字列化された同じオブジェクトの中でプロパティの順番に依存しないようにしてください。

とのことでしたので、JSON.stringifyをObjectに適用する方法ではなく、es6より実装された`Object.entries`を利用し、配列に適用する方法に修正しております。

JavaScriptのObjectとは

 JavaScriptのObjectは、他の言語における、いわゆる「連想配列」です。

こうしたい

const a = {"a":"a"};
const b = {"a":"a"};

console.log(a === b);  // -> trueであってほしい!

 実行すると、a === bはfalseであることがわかります。

等価演算子の動作

 Javaと違って、JavaScriptはObject.equalsメソッドを持ちません。かといって、オブジェクト同士の演算に===演算子を使用しても、それは「同一のインスタンスかどうか」を判断するものとなります。

const a = {"a":"a"};
const b = a;

console.log(a === b);  // -> true

 もちろん、これらは等価となるためtrueが返されます。

 しかし、冒頭で挙げた例のように、同一の内容を持った異なるインスタンスを比較してもfalseとなります。

 これは簡単に言えば、中身だけではなく、外側の入れ物も含めて同一かどうかを比較しているわけです。

 Javaなどの他のオブジェクト指向言語でも同じ挙動となるはずです。ある意味、オブジェクト指向言語の大原則といってもよいでしょう。

JavaScriptでObjectの内容を比較する方法

JSON.stringifyを使う

const a = {"a":"a"};
const b = {"a":"a"};

const aJSON = JSON.stringify(a);
const bJSON = JSON.stringify(b);

console.log(aJSON === bJSON);  // -> true

 しかし、以下のようなオブジェクトの場合、falseが返されます。

const a = {"a":"a","b":"b"};
const b = {"b":"b","a":"a"};

const aJSON = JSON.stringify(a);
const bJSON = JSON.stringify(b);

console.log(aJSON === bJSON);  // -> false

 オブジェクト順序は保証されていませんから、これらの順序をどうにかしても、JSON.stringifyによって比較するのは無理筋に見えます。

中身を全て比較する

 古典的に、オブジェクトの中身を全て検査する方法です。

 本例のコードは非常に簡略化されています。使用する際は仕様に応じて変更の上、十分にテストを行ってから使用してください。
function objectEquals(a, b){

    if(a === b){
        // 同一インスタンスならtrueを返す
        return true;
    }

    // 比較対象双方のキー配列を取得する(順番保証のためソートをかける)
    const aKeys = Object.keys(a).sort();
    const bKeys = Object.keys(b).sort();

    // 比較対象同士のキー配列を比較する
    if(aKeys.toString() !== bKeys.toString()){
        // キーが違う場合はfalse
        return false;
    }

    // 値をすべて調べる。
    const wrongIndex = aKeys.findIndex(function(value){
        // 注意!これは等価演算子で正常に比較できるもののみを対象としています。
        // つまり、ネストされたObjectやArrayなどには対応していないことに注意してください。
        return a[value] !== b[value];
    });

    // 合致しないvalueがなければ、trueを返す。
    return wrongIndex === -1;
}

const a = {"a":"a","b":"b"};
const b = {"b":"b","a":"a"};

console.log(objectEquals(a, b));  // -> true

const c = {"a":"a","b":"c"};
const d = {"a":"a","b":"b"};

console.log(objectEquals(c, d));  // -> false

 複雑なコードになりましたが、順序が定まっていないオブジェクトに対する検査としての要件は満たしています。こういったユーティリティ的なメソッドを作成しておくのもひとつの手です。

 ですが、もう少しシンプルに考えてみましょう。

Object.entriesで配列化し、ソートを行った上でJSON.stringifyを使う

 JSON.stringifyを使ったときに問題だったのは、JSON.stringifyの順序性が保証されておらず、結果が同一とならない可能性があったことです。であれば、オブジェクトを配列に変換した上でソートし、両者を比較してみましょう。

 配列は順序性が保証されており、必ず同じ順番で文字列化されます。

const a = {"a":"a","b":"b"};
const b = {"b":"b","a":"a"};

const aJSON = JSON.stringify(Object.entries(a).sort());
const bJSON = JSON.stringify(Object.entries(b).sort());

console.log(aJSON === bJSON);  // -> true

 しかし、これでもやはり内部のオブジェクトには対応できません。対応するには、再帰処理を使って以下のようにします。

function objectSort(obj){

    // ソートする
    const sorted = Object.entries(obj).sort();

    // valueを調べ、objectならsorted entriesに変換する
    for(let i in sorted){
        const val = sorted[i][1];
        if(typeof val === "object"){
            sorted[i][1] = objectSort(val);
        }
    }

    return sorted;
}

const a = {"a":"a","b":"b","o":{"a":"a","b":"b"}};
const b = {"b":"b","a":"a","o":{"b":"b","a":"a"}};

const aJSON = JSON.stringify(objectSort(a));
const bJSON = JSON.stringify(objectSort(b));

console.log(aJSON === bJSON);  // -> true
JSON.stringifyではなく、toStringを使うのも一見よさそうに見えますが、以下の例を見てみると適切でないことがわかります。
const a = {"a":"a","b":["b", "b"],"o":{"a":"a","b":"b"}};
const b = {"a":"a","b":"b,b","o":{"a":"a","b":"b"}};


const aJSON = Object.entries(a).sort().toString();
const bJSON = Object.entries(b).sort().toString();

console.log(aJSON === bJSON);  // -> true

まとめ

 JavaScriptのObjectやJSON.stringifyなどは、ブラウザ依存の実装も多いのですが、少し工夫することで使えるようになったりします。

 複雑なメソッドを書かなくても要件を満たす動作をしてくれる場合は、簡単な方を選びたいものですね。

コメント

  1. 匿名 より:

    「ソートを行った上でJSON.stringifyを使う」ですが、
    「オブジェクトに格納した順番に文字列化される」保証もないのではないでしょうか。

    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

    > Note: Properties of non-array objects are not guaranteed to be stringified in any particular order. Do not rely on ordering of properties within the same object within the stringification.

    • tam-tam より:

      ご指摘ありがとうございます。おっしゃる通り、object順序については仕様に無いので、ブラウザ実装による、という形になっていますね。

      記事を修正させていただきました。