プリミティブなもの同士の比較になるようにオブジェクト(もしくは配列)のプロパティを再帰しながら、たどればいいようです。
以下は30 seconds of codeより引用。
const equals = (a, b) => {
if (a === b) return true;
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
if (!a || !b || (typeof a != 'object' && typeof b !== 'object')) return a === b;
if (a === null || a === undefined || b === null || b === undefined) return false;
if (a.prototype !== b.prototype) return false;
let keys = Object.keys(a);
if (keys.length !== Object.keys(b).length) return false;
return keys.every(k => equals(a[k], b[k]));
};
equals({ a: [2, { e: 3 }], b: [4], c: 'foo' }, { a: [2, { e: 3 }], b: [4], c: 'foo' }); // true
日付が特別扱いされているので、そういう意味では状況によっては改良が必要かもしれないですね。
あとは、Object.keysを使って配列とオブジェクトを同じふうに扱っているのが興味深いと思いました。
追記
コメントでこのままでは正しく動かないケースが多いと、ご指摘いただきました。
そのため、実際にこのまま使うということは避けたほうがいいと思います。
うまくいくためにはどうすればいいか修正を検討してみます。
追記2
原因を調査してみました。
prototypeによる問題が多いので、そこらへんを厳密に比較すれば対応できそうな感じはします。
あとは配列とオブジェクト、文字の同一視を利用して不正ができるみたいです。
頑張れば、対応できそうではあるので、もし対応すれば追記します。
// elementはprototypeではなく、__proto__で型の判定をしないといけない
// prototypeの比較の際に両方を見る必要がある?
equals(document.createElement('a'), {}); // true
// 自分の環境では再現しなかった
equals({prototype: NaN, a:2}, {prototype:1, a:3}); // true
// オブジェクトと配列を同一視しているため
// 後半部で型を厳密にチェックしたうえで、再帰が必要
equals({0:1}, [1]); // true
// NaNは同士の比較はfalseになるため
// Number.isNaNを使って、厳密に判定する必要がある
equals(NaN, NaN); // false
// MapはIterater経由でしか、値を取得できないため
equals(new Map([['a', 1]]), new Map([['b', 1]]))
// 厳密には不明
// instanceofで日付型と判定されるのに、日付が入っていないオブジェクトを渡しているため?
// しかし、判定方法がわからない
equals(Object.create(Date.prototype), Object.create(Date.prototype));
// 数値・真偽値は__proto__で判定が必要
console.log(equals(1, {})); // true
console.log(equals(true, {})); // true
// 配列と文字列は同じアクセス方法でアクセスできるため
console.log(equals('a', {0: 'a'})); //true
// Symbolも__proto__で判定が必要
console.log(equals(Symbol('a'), {})); // true
追記3
型の判定を追加すれば、多くのケースでうまくいきました。あとは、NaN対応も入れました。
DateをObject.createで作ったケースと、Mapのケースは特別扱いしないとうまくいかないので、対応していないです。
そもそも同値性をどう定義するのかは場合によって違うので、どちらにせよこのまま使うというのは考えづらいので、そこだけ直して終わりにします。
const equals = (a, b) => {
if (a === b) return true;
// NaNのケースに対応
if (Number.isNaN(a) && Number.isNaN(b)) return true;
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
if (!a || !b || (typeof a != 'object' && typeof b !== 'object')) return a === b;
if (a === null || a === undefined || b === null || b === undefined) return false;
if (a.prototype !== b.prototype) return false;
// prototypeを取得して型を比較
// __proto__は公式準拠ではないらしいので不使用
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
let keys = Object.keys(a);
if (keys.length !== Object.keys(b).length) return false;
return keys.every(k => equals(a[k], b[k]));
};