JavaScriptでのObject比較方法

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

スポンサーリンク
rectangle

JavaScriptのObjectとは

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

こうしたい

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

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

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

等価演算子の動作

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

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

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

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

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

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

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

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

JSON.stringifyを使う

 オブジェクトの内容の順序が保証されている場合は、コストは別として以下のようにすれば楽に比較できます。

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

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

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

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

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

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

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

 これは、もしかするとシステム仕様的に期待する動作ではないかもしれません。そこで、次のような手段を考えてみましょう。

中身を全て比較する

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

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

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

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

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

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

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

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

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

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

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

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

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

ソートを行った上でJSON.stringifyを使う

 JSON.stringifyを使ったときに問題だったのは、JSONの順序が違う場合にstringifyの結果が同一とならなかったことです。であれば、オブジェクトをソートしてあげればよいのです。以下のようにしましょう。

function objectSort(obj){
    // まずキーのみをソートする
    var keys = Object.keys(obj).sort();

    // 返却する空のオブジェクトを作る
    var map = {};

    // ソート済みのキー順に返却用のオブジェクトに値を格納する
    keys.forEach(function(key){
        map[key] = obj[key];
    });

    return map;
}

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

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

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

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

function objectSort(obj){
    // まずキーのみをソートする
    var keys = Object.keys(obj).sort();

    // 返却する空のオブジェクトを作る
    var map = {};

    // ソート済みのキー順に返却用のオブジェクトに値を格納する
    keys.forEach(function(key){
        var val = obj[key];

        // 中身がオブジェクトの場合は再帰呼び出しを行う
        if(typeof val === "object"){
            val = objectSort(val);
        }

        map[key] = val;
    });

    return map;
}

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

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

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

まとめ

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

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