JavaScriptでオブジェクトを比較するための最良の方法は何ですか?
例:
var user1 = {name : "nerd", org: "dev"};
var user2 = {name : "nerd", org: "dev"};
var eq = user1 == user2;
alert(eq); // gives false
2つのオブジェクトがまったく同じオブジェクトを参照していれば等しいということを私は知っています しかし、それらが同じ属性の値を持っているかどうかを調べる方法はありますか?
次の方法は私のために働きます、しかしそれは唯一の可能性ですか?
var eq = Object.toJSON(user1) == Object.toJSON(user2);
alert(eq); // gives true
残念ながら、_proto_
を再帰的に使用して、列挙不可能なすべてのプロパティにアクセスしない限り、完璧な方法はありませんが、これはFirefoxでのみ機能します。
だから私ができる最善は使用シナリオを推測することです。
内部にメソッドやDOMノードがない単純なJSONスタイルのオブジェクトがある場合に機能します。
JSON.stringify(obj1) === JSON.stringify(obj2)
プロパティの順序IS重要。したがって、このメソッドは次のオブジェクトに対してfalseを返します。
x = {a: 1, b: 2};
y = {b: 2, a: 1};
プロトタイプを調べずにオブジェクトを比較してから、プロパティーの射影を再帰的に比較し、さらにコンストラクターを比較します。
これはほぼ正しいアルゴリズムです。
function deepCompare () {
var i, l, leftChain, rightChain;
function compare2Objects (x, y) {
var p;
// remember that NaN === NaN returns false
// and isNaN(undefined) returns true
if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
return true;
}
// Compare primitives and functions.
// Check if both arguments link to the same object.
// Especially useful on the step where we compare prototypes
if (x === y) {
return true;
}
// Works in case when functions are created in constructor.
// Comparing dates is a common scenario. Another built-ins?
// We can even handle functions passed across iframes
if ((typeof x === 'function' && typeof y === 'function') ||
(x instanceof Date && y instanceof Date) ||
(x instanceof RegExp && y instanceof RegExp) ||
(x instanceof String && y instanceof String) ||
(x instanceof Number && y instanceof Number)) {
return x.toString() === y.toString();
}
// At last checking prototypes as good as we can
if (!(x instanceof Object && y instanceof Object)) {
return false;
}
if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) {
return false;
}
if (x.constructor !== y.constructor) {
return false;
}
if (x.prototype !== y.prototype) {
return false;
}
// Check for infinitive linking loops
if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) {
return false;
}
// Quick checking of one object being a subset of another.
// todo: cache the structure of arguments[0] for performance
for (p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
else if (typeof y[p] !== typeof x[p]) {
return false;
}
}
for (p in x) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
else if (typeof y[p] !== typeof x[p]) {
return false;
}
switch (typeof (x[p])) {
case 'object':
case 'function':
leftChain.Push(x);
rightChain.Push(y);
if (!compare2Objects (x[p], y[p])) {
return false;
}
leftChain.pop();
rightChain.pop();
break;
default:
if (x[p] !== y[p]) {
return false;
}
break;
}
}
return true;
}
if (arguments.length < 1) {
return true; //Die silently? Don't know how to handle such case, please help...
// throw "Need two or more arguments to compare";
}
for (i = 1, l = arguments.length; i < l; i++) {
leftChain = []; //Todo: this can be cached
rightChain = [];
if (!compare2Objects(arguments[0], arguments[i])) {
return false;
}
}
return true;
}
既知の問題(まあ、それらは非常に低い優先順位を持っています、おそらくあなたはそれらに決して気付かないでしょう):
テスト: テストは2つのJavaScriptオブジェクトが等しいかどうかを判断する方法?からの合格です。
ES3でコメントしたソリューションを以下に示します(コードの後に詳細を記載):
Object.equals = function( x, y ) {
if ( x === y ) return true;
// if both x and y are null or undefined and exactly the same
if ( ! ( x instanceof Object ) || ! ( y instanceof Object ) ) return false;
// if they are not strictly equal, they both need to be Objects
if ( x.constructor !== y.constructor ) return false;
// they must have the exact same prototype chain, the closest we can do is
// test there constructor.
for ( var p in x ) {
if ( ! x.hasOwnProperty( p ) ) continue;
// other properties were tested using x.constructor === y.constructor
if ( ! y.hasOwnProperty( p ) ) return false;
// allows to compare x[ p ] and y[ p ] when set to undefined
if ( x[ p ] === y[ p ] ) continue;
// if they have the same strict value or identity then they are equal
if ( typeof( x[ p ] ) !== "object" ) return false;
// Numbers, Strings, Functions, Booleans must be strictly equal
if ( ! Object.equals( x[ p ], y[ p ] ) ) return false;
// Objects and Arrays must be tested recursively
}
for ( p in y ) {
if ( y.hasOwnProperty( p ) && ! x.hasOwnProperty( p ) ) return false;
// allows x[ p ] to be set to undefined
}
return true;
}
このソリューションを開発する際に、コーナーケース、効率性に特に注目しましたが、うまく機能するシンプルなソリューションを生み出そうとしました。 JavaScriptでは、nullとndefinedプロパティとオブジェクトにはプロトタイプチェーンがあり、チェックしないと動作が大きく異なる可能性があります。
まず、Object.prototypeの代わりにObjectを拡張することを選択しました。これは主にnullが比較のオブジェクトの1つでなかったためです。そして、私はnullは別のものと比較するための有効なオブジェクトであるべきだと信じています。Object.prototypeの可能性に関して、他の人が指摘した他の正当な懸念もあります他のコードへの副作用。
JavaScriptがオブジェクトのプロパティをndefinedに設定できるという可能性に対処するために特別な注意が必要です。つまり、値がndefinedに設定されるプロパティが存在します。両方のオブジェクトに同じプロパティがndefinedに設定されており、同等であることを報告します。これは、Object.hasOwnProperty(property_name)を使用してプロパティの存在をチェックすることによってのみ実現できますJSON.stringify()はndefinedに設定されたプロパティを削除するため、このフォームを使用した比較では、値に設定されたプロパティは無視されますndefined。
関数は、同じコードだけでなく同じ参照を共有する場合にのみ等しいと見なされる必要があります。これは、これらの関数のプロトタイプが考慮されないためです。そのため、コード文字列を比較しても、同じプロトタイプオブジェクトがあることを保証できません。
2つのオブジェクトは、同じプロパティだけでなく、同じプロトタイプチェーンを持っている必要があります。これは、両方のオブジェクトのconstructorECMAScript 5では、Object.getPrototypeOf()を使用して実際のプロトタイプをテストできます。一部のWebブラウザーは、同じことを行う_PROTO_プロパティも提供します。上記のコードの改善点利用可能な場合はいつでもこれらの方法のいずれかを使用できます。
2は"2.0000"とは見なされず、falseは等しいと見なされる必要があるため、ここでは厳密な比較を使用しますto null、ndefined、または。
効率を考慮すると、できるだけ早くプロパティの同等性を比較することになります。次に、それが失敗した場合にのみ、typeofこれらのプロパティを探します。速度の向上は、多数のスカラープロパティを持つ大きなオブジェクトで重要になる可能性があります。
2つのループは必要ありません。1つ目は左側のオブジェクトのプロパティをチェックし、2つ目は右側のプロパティをチェックし、存在のみ(値ではない)を確認し、ndefined値。
全体的に、このコードはわずか16行のコード(コメントなし)でほとんどのコーナーケースを処理します。
更新(2015年8月13日)。 value_equals() 関数がより高速で、NaNや0などの0以外の適切なコーナーケースを適切に処理するため、より良いバージョンを実装しました、オプションでオブジェクトのプロパティの順序を強制し、 Toubkal プロジェクトテストスイートの一部として 100自動化テスト を超える数の循環参照をテストします。
Utils.compareObjects = function(o1, o2){
for(var p in o1){
if(o1.hasOwnProperty(p)){
if(o1[p] !== o2[p]){
return false;
}
}
}
for(var p in o2){
if(o2.hasOwnProperty(p)){
if(o1[p] !== o2[p]){
return false;
}
}
}
return true;
};
1レベルのみのオブジェクトを比較する簡単な方法。
確かに唯一の方法ではありません - C#/ Javaスタイルの比較メソッドを複製するためのメソッドをプロトタイプ化することができます(ここではObjectに対してライブオブジェクトにObjectを使用することはお勧めしません)。
一般的な例が予想されるので編集してください。
Object.prototype.equals = function(x)
{
for(p in this)
{
switch(typeof(this[p]))
{
case 'object':
if (!this[p].equals(x[p])) { return false }; break;
case 'function':
if (typeof(x[p])=='undefined' || (p != 'equals' && this[p].toString() != x[p].toString())) { return false; }; break;
default:
if (this[p] != x[p]) { return false; }
}
}
for(p in x)
{
if(typeof(this[p])=='undefined') {return false;}
}
return true;
}
ToString()でメソッドをテストするのは 絶対に十分ではありません しかし、意味のある空白を持たないかどうかの問題のために受け入れられるメソッドは非常に難しいです。実装 そして Objectに対するプロトタイピングの問題。
次のアルゴリズムは、自己参照データ構造、数値、文字列、日付、そしてもちろんプレーンなネストされたJavaScriptオブジェクトを扱います。
以下の場合、オブジェクトは同等と見なされます。
===
ごとに正確に同じです(42
がNumber(42)
と同等であることを保証するために最初にStringとNumberがアンラップされます)。valueOf()
を持っています==
ごとに同じです(数字/文字列/ブール値を受け取ります)。undefined
の値を持つプロパティを無視すると、それらは同じプロパティを持ち、それらすべては再帰的に等価であると見なされます。 機能 機能テキストで同一とは見なされません。機能ごとにクロージャが異なる可能性があるため、このテストは不十分です。 ===
がそうである場合にのみ、関数は等しいと見なされます(しかし、あなたがそうすることを選択すれば、あなたはその等価な関係を容易に拡張することができます)。
無限ループ 潜在的に循環データ構造によって引き起こされる - は避けられる。 areEquivalent
が等価性を反証しようとし、そうするためにオブジェクトのプロパティに再帰すると、このサブ比較が必要なオブジェクトを追跡します。同等性が証明できない場合、到達可能なプロパティパスがオブジェクト間で異なるため、到達可能な最短パスが存在しなければならず、その最短到達可能パスに両方のパスに存在するサイクルを含めることはできません。つまり、オブジェクトを再帰的に比較するときに同等であると見なして構いません。この仮定は使用後に削除されるプロパティareEquivalent_Eq_91_2_34
に格納されますが、オブジェクトグラフがすでにそのようなプロパティを含んでいる場合の動作は未定義です。 javascriptは任意のオブジェクトをキーとして使用する辞書をサポートしていないため、そのようなマーカープロパティの使用は必要です。
function unwrapStringOrNumber(obj) {
return (obj instanceof Number || obj instanceof String
? obj.valueOf()
: obj);
}
function areEquivalent(a, b) {
a = unwrapStringOrNumber(a);
b = unwrapStringOrNumber(b);
if (a === b) return true; //e.g. a and b both null
if (a === null || b === null || typeof (a) !== typeof (b)) return false;
if (a instanceof Date)
return b instanceof Date && a.valueOf() === b.valueOf();
if (typeof (a) !== "object")
return a == b; //for boolean, number, string, xml
var newA = (a.areEquivalent_Eq_91_2_34 === undefined),
newB = (b.areEquivalent_Eq_91_2_34 === undefined);
try {
if (newA) a.areEquivalent_Eq_91_2_34 = [];
else if (a.areEquivalent_Eq_91_2_34.some(
function (other) { return other === b; })) return true;
if (newB) b.areEquivalent_Eq_91_2_34 = [];
else if (b.areEquivalent_Eq_91_2_34.some(
function (other) { return other === a; })) return true;
a.areEquivalent_Eq_91_2_34.Push(b);
b.areEquivalent_Eq_91_2_34.Push(a);
var tmp = {};
for (var prop in a)
if(prop != "areEquivalent_Eq_91_2_34")
tmp[prop] = null;
for (var prop in b)
if (prop != "areEquivalent_Eq_91_2_34")
tmp[prop] = null;
for (var prop in tmp)
if (!areEquivalent(a[prop], b[prop]))
return false;
return true;
} finally {
if (newA) delete a.areEquivalent_Eq_91_2_34;
if (newB) delete b.areEquivalent_Eq_91_2_34;
}
}
オブジェクト比較のためにこのコードを書きましたが、うまくいくようです。アサーションを確認してください。
function countProps(obj) {
var count = 0;
for (k in obj) {
if (obj.hasOwnProperty(k)) {
count++;
}
}
return count;
};
function objectEquals(v1, v2) {
if (typeof(v1) !== typeof(v2)) {
return false;
}
if (typeof(v1) === "function") {
return v1.toString() === v2.toString();
}
if (v1 instanceof Object && v2 instanceof Object) {
if (countProps(v1) !== countProps(v2)) {
return false;
}
var r = true;
for (k in v1) {
r = objectEquals(v1[k], v2[k]);
if (!r) {
return false;
}
}
return true;
} else {
return v1 === v2;
}
}
assert.isTrue(objectEquals(null,null));
assert.isFalse(objectEquals(null,undefined));
assert.isTrue(objectEquals("hi","hi"));
assert.isTrue(objectEquals(5,5));
assert.isFalse(objectEquals(5,10));
assert.isTrue(objectEquals([],[]));
assert.isTrue(objectEquals([1,2],[1,2]));
assert.isFalse(objectEquals([1,2],[2,1]));
assert.isFalse(objectEquals([1,2],[1,2,3]));
assert.isTrue(objectEquals({},{}));
assert.isTrue(objectEquals({a:1,b:2},{a:1,b:2}));
assert.isTrue(objectEquals({a:1,b:2},{b:2,a:1}));
assert.isFalse(objectEquals({a:1,b:2},{a:1,b:3}));
assert.isTrue(objectEquals({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}},{1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}));
assert.isFalse(objectEquals({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}},{1:{name:"mhc",age:28}, 2:{name:"arb",age:27}}));
assert.isTrue(objectEquals(function(x){return x;},function(x){return x;}));
assert.isFalse(objectEquals(function(x){return x;},function(y){return y+2;}));
上記のコードを少し修正しました。私のために 0!== false そして null!==未定義 。このような厳密なチェックが不要な場合は、コード内の " this [p]!== x [p] "で " = "サインインしてください.
Object.prototype.equals = function(x){
for (var p in this) {
if(typeof(this[p]) !== typeof(x[p])) return false;
if((this[p]===null) !== (x[p]===null)) return false;
switch (typeof(this[p])) {
case 'undefined':
if (typeof(x[p]) != 'undefined') return false;
break;
case 'object':
if(this[p]!==null && x[p]!==null && (this[p].constructor.toString() !== x[p].constructor.toString() || !this[p].equals(x[p]))) return false;
break;
case 'function':
if (p != 'equals' && this[p].toString() != x[p].toString()) return false;
break;
default:
if (this[p] !== x[p]) return false;
}
}
return true;
}
それから私は次のオブジェクトでそれをテストしました:
var a = {a: 'text', b:[0,1]};
var b = {a: 'text', b:[0,1]};
var c = {a: 'text', b: 0};
var d = {a: 'text', b: false};
var e = {a: 'text', b:[1,0]};
var f = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
var g = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
var h = {a: 'text', b:[1,0], f: function(){ this.a = this.b; }};
var i = {
a: 'text',
c: {
b: [1, 0],
f: function(){
this.a = this.b;
}
}
};
var j = {
a: 'text',
c: {
b: [1, 0],
f: function(){
this.a = this.b;
}
}
};
var k = {a: 'text', b: null};
var l = {a: 'text', b: undefined};
a == bはtrueと予想されます。真を返した
a == cはfalseを想定していました。 falseを返しました
c == dはfalseを想定しています。 falseを返しました
a == eはfalseと予想されます。 falseを返しました
f == gは真と予想される。真を返した
h == gはfalseと予想されます。 falseを返しました
i == jはtrueと予想されます。真を返した
d == kはfalseと予想されます。 falseを返しました
k == lは偽と予想される。 falseを返しました
メソッドを明示的にチェックしたい場合は、method.toSource()またはmethod.toString()メソッドを使用できます。
これが私のバージョンです。このスレッドからのほとんどのものが統合されています(テストケースについても同じです)。
Object.defineProperty(Object.prototype, "equals", {
enumerable: false,
value: function (obj) {
var p;
if (this === obj) {
return true;
}
// some checks for native types first
// function and sring
if (typeof(this) === "function" || typeof(this) === "string" || this instanceof String) {
return this.toString() === obj.toString();
}
// number
if (this instanceof Number || typeof(this) === "number") {
if (obj instanceof Number || typeof(obj) === "number") {
return this.valueOf() === obj.valueOf();
}
return false;
}
// null.equals(null) and undefined.equals(undefined) do not inherit from the
// Object.prototype so we can return false when they are passed as obj
if (typeof(this) !== typeof(obj) || obj === null || typeof(obj) === "undefined") {
return false;
}
function sort (o) {
var result = {};
if (typeof o !== "object") {
return o;
}
Object.keys(o).sort().forEach(function (key) {
result[key] = sort(o[key]);
});
return result;
}
if (typeof(this) === "object") {
if (Array.isArray(this)) { // check on arrays
return JSON.stringify(this) === JSON.stringify(obj);
} else { // anyway objects
for (p in this) {
if (typeof(this[p]) !== typeof(obj[p])) {
return false;
}
if ((this[p] === null) !== (obj[p] === null)) {
return false;
}
switch (typeof(this[p])) {
case 'undefined':
if (typeof(obj[p]) !== 'undefined') {
return false;
}
break;
case 'object':
if (this[p] !== null
&& obj[p] !== null
&& (this[p].constructor.toString() !== obj[p].constructor.toString()
|| !this[p].equals(obj[p]))) {
return false;
}
break;
case 'function':
if (this[p].toString() !== obj[p].toString()) {
return false;
}
break;
default:
if (this[p] !== obj[p]) {
return false;
}
}
};
}
}
// at least check them with JSON
return JSON.stringify(sort(this)) === JSON.stringify(sort(obj));
}
});
これが私のテストケースです。
assertFalse({}.equals(null));
assertFalse({}.equals(undefined));
assertTrue("String", "hi".equals("hi"));
assertTrue("Number", new Number(5).equals(5));
assertFalse("Number", new Number(5).equals(10));
assertFalse("Number+String", new Number(1).equals("1"));
assertTrue([].equals([]));
assertTrue([1,2].equals([1,2]));
assertFalse([1,2].equals([2,1]));
assertFalse([1,2].equals([1,2,3]));
assertTrue(new Date("2011-03-31").equals(new Date("2011-03-31")));
assertFalse(new Date("2011-03-31").equals(new Date("1970-01-01")));
assertTrue({}.equals({}));
assertTrue({a:1,b:2}.equals({a:1,b:2}));
assertTrue({a:1,b:2}.equals({b:2,a:1}));
assertFalse({a:1,b:2}.equals({a:1,b:3}));
assertTrue({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}.equals({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}));
assertFalse({1:{name:"mhc",age:28}, 2:{name:"arb",age:26}}.equals({1:{name:"mhc",age:28}, 2:{name:"arb",age:27}}));
assertTrue("Function", (function(x){return x;}).equals(function(x){return x;}));
assertFalse("Function", (function(x){return x;}).equals(function(y){return y+2;}));
var a = {a: 'text', b:[0,1]};
var b = {a: 'text', b:[0,1]};
var c = {a: 'text', b: 0};
var d = {a: 'text', b: false};
var e = {a: 'text', b:[1,0]};
var f = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
var g = {a: 'text', b:[1,0], f: function(){ this.f = this.b; }};
var h = {a: 'text', b:[1,0], f: function(){ this.a = this.b; }};
var i = {
a: 'text',
c: {
b: [1, 0],
f: function(){
this.a = this.b;
}
}
};
var j = {
a: 'text',
c: {
b: [1, 0],
f: function(){
this.a = this.b;
}
}
};
var k = {a: 'text', b: null};
var l = {a: 'text', b: undefined};
assertTrue(a.equals(b));
assertFalse(a.equals(c));
assertFalse(c.equals(d));
assertFalse(a.equals(e));
assertTrue(f.equals(g));
assertFalse(h.equals(g));
assertTrue(i.equals(j));
assertFalse(d.equals(k));
assertFalse(k.equals(l));
あなたがJSONライブラリなしで作業しているなら、多分これはあなたを助けるでしょう:
Object.prototype.equals = function(b) {
var a = this;
for(i in a) {
if(typeof b[i] == 'undefined') {
return false;
}
if(typeof b[i] == 'object') {
if(!b[i].equals(a[i])) {
return false;
}
}
if(b[i] != a[i]) {
return false;
}
}
for(i in b) {
if(typeof a[i] == 'undefined') {
return false;
}
if(typeof a[i] == 'object') {
if(!a[i].equals(b[i])) {
return false;
}
}
if(a[i] != b[i]) {
return false;
}
}
return true;
}
var a = {foo:'bar', bar: {blub:'bla'}};
var b = {foo:'bar', bar: {blub:'blob'}};
alert(a.equals(b)); // alert's a false