他の言語とはひと味違う、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
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などは、ブラウザ依存の実装も多いのですが、少し工夫することで使えるようになったりします。
複雑なメソッドを書かなくても要件を満たす動作をしてくれる場合は、簡単な方を選びたいものですね。