(追記)
コメントで指摘のあった循環参照で無限ループする問題を解決しました
そのほかにも細かなバグが有ったので修正しています。
githubに公開しましたので、実際のソースはそちらで確認してください結局underscoreのisEqualとほぼ同じ動きになっています。
ただ、一部Invaild Dateで結果が異なっている等あるみたいですが!
==
javascriptの等価演算子では==と===が存在しますが、
人間らしい演算子を行うcompare関数を作成してみました。
実際に、私も実務でテストで使用しています。
例えば、{a: 1} == {a: 1} としても {a: 1} === {a: 1} としても結果はfalseです
流石に、new Date('2013-9-1') == new Date('2013-9-1') くらいはtrueかなと思ってもやっぱりfalseです
しかし等価として扱いたい場合があるので==や===の代わりにcompare(x, y)でbooleanを返すようにしてみました。
先にcompareで使用する為の型取得用のgetType関数を定義します。
null,undefined以外はconstructorを返すだけの簡単な関数です。
var getType = function getType (target) {
if (target === null || target === undefined) {
return target;
} else {
return target.constructor;
}
};
次に今回のcompare関数を定義します。
あらゆる値を受け付けるため、switchで比較の方法を制御しています。
コンストラクタの比較を先に行い、その後値の比較を行います。
オブジェクトはさらに値を再帰で処理しています。
var compare = function compare (target1, target2) {
if (target1 === target2) {
return true;
}
var tp1 = getType(target1)
, tp2 = getType(target2);
if (tp1 !== tp2) {
return false;
}
switch(true) {
case tp1 === Boolean:
case tp1 === String:
return target1 == target2;
case tp1 === Number:
return target1 == target2 || isNaN(target1) && isNaN(target2);
case tp1 === RegExp:
return target1.toString() === target2.toString();
case tp1 === Date:
var t1 = target1.getTime()
, t2 = target2.getTime();
return t1 === t2 || isNaN(t1) && isNaN(t2);
case tp1 === Function:
return false;
case target1 instanceof Error:
return target1.message === target2.message;
default:
var keys1 = Object.keys(target1)
, keys2 = Object.keys(target2);
if (keys1.length !== keys2.length) {
return false;
}
keys1.sort();
keys2.sort();
return keys1.every(function(key, i) {
return key === keys2[i] && compare(target1[key], target2[key]);
});
}
};
node.jsで以下のテストコードを記述しました
var assert = require('assert');
// null, undefinedの比較
assert( compare(null, null));
assert( compare(undefined, undefined));
assert(!compare(null, undefined));
// 真偽値の比較
assert( compare(true, true));
assert(!compare(true, false));
assert( compare(true, new Boolean(1)));
assert(!compare(true, 1));
// 数値の比較
assert( compare(1, 1));
assert( compare(1, new Number(1)));
assert( compare(Infinity, Number.POSITIVE_INFINITY));
assert( compare(Infinity, 1/0));
assert( compare(new Number('hoge'), NaN));
// 文字列の比較
assert( compare('abc', 'abc'));
assert(!compare('abc', 'def'));
assert( compare('abc', new String('abc')));
// 日付の比較
assert( compare(new Date('2013-9-1'), new Date('2013-9-1')));
assert(!compare(new Date('2013-9-1'), new Date('2013-9-2')));
assert( compare(new Date('hoge'), new Date('hage')));
// 正規表現の比較
assert( compare(/abc/, /abc/));
assert(!compare(/abc/, /abc/g));
assert( compare(/abc/, new RegExp(/abc/)));
// エラーの比較
assert( compare(Error('error1'), Error('error1')));
assert(!compare(Error('error1'), Error('error2')));
// オブジェクトの比較 1
assert( compare({a: 1}, {a: 1}));
assert(!compare({a: 1}, {a: 2}));
assert( compare({a: 1, b: 2}, {b: 2, a: 1}));
// 関数の比較
var fn1 = function fn1 () {};
var fn2 = function fn2 () {};
assert( compare(fn1, fn1));
assert(!compare(fn1, fn2));
// オブジェクトの比較 2
var ob11 = new fn1();
var ob12 = new fn1();
var ob21 = new fn2();
assert( compare(ob11, ob12));
assert(!compare(ob11, ob21));
// オブジェクトの比較 3
var fn3 = function fn3 (name) { this.name = name; };
fn3.prototype.name = null;
var ob31 = new fn3('toyota');
var ob32 = new fn3('honda');
var ob33 = new fn3('honda');
assert(!compare(ob31, ob32));
assert( compare(ob32, ob33));
// 複雑なオブジェクトの比較
var ob1 = {
a: 1
, b: new Date('2013-9-1')
, c: {d: new fn1()}
};
var ob2 = {
a: 1
, b: new Date('2013-9-1')
, c: {d: new fn1()}
};
var ob3 = { x: ob1, y: ob2 };
ob1.z = ob3;
ob2.z = ob3;
assert( compare(ob1, ob2));
console.log('test pass');
「複雑なオブジェクトの比較」で循環しているオブジェクトの比較も成功しています。
もちろんフロントエンドでも使用出来るので、jQueryなどで本当に意図したエレメントを取得しているかなどの確認にも使用出来るはずです。