Object comparison in JavaScript
在javascript中比较对象的最佳方法是什么?
例子:
1 2 3 4 | var user1 = {name :"nerd", org:"dev"}; var user2 = {name :"nerd", org:"dev"}; var eq = user1 == user2; alert(eq); // gives false |
我知道如果两个对象引用完全相同的对象,那么它们是相等的,但是有没有办法检查它们是否具有相同的属性值?
下面的方法对我有效,但这是唯一的可能性吗?
1 2 | var eq = Object.toJSON(user1) == Object.toJSON(user2); alert(eq); // gives true |
不幸的是,没有完美的方法,除非您递归地使用
所以我能做的最好的就是猜测使用场景。
1)快速和有限。如果您有简单的JSON样式的对象,但其中没有方法和DOM节点,则可以使用:
1 | JSON.stringify(obj1) === JSON.stringify(obj2) |
属性的顺序很重要,因此对于以下对象,此方法将返回false:
1 2 | x = {a: 1, b: 2}; y = {b: 2, a: 1}; |
2)速度慢,通用性强。
比较对象而不挖掘原型,然后递归比较属性的投影,还比较构造函数。
这几乎是正确的算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | 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; } |
已知问题(它们的优先级很低,可能您永远不会注意到它们):
- 具有不同原型结构但投影相同的对象
- 函数可以有相同的文本,但引用不同的闭包
测试:通过测试是从如何确定两个javascript对象的相等性开始的?.
下面是我在ES3中的注释解决方案(代码后的详细信息):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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允许空属性和未定义属性,并且对象具有原型链,如果不选中,则可能导致非常不同的行为。
首先,我选择扩展object而不是object.prototype,主要是因为null不能是比较的对象之一,而且我认为null应该是一个有效的对象来与另一个对象进行比较。还有其他人注意到的关于object.prototype的扩展的其他合法问题,这些问题涉及对其他代码可能产生的副作用。
必须特别注意处理javascript允许将对象属性设置为未定义的可能性,即存在值设置为未定义的属性。上面的解决方案验证两个对象是否具有相同的属性设置为"未定义"以报告相等性。这只能通过使用object.hasownProperty(property_name)检查属性的存在来完成。还要注意,json.stringify()将删除设置为未定义的属性,因此使用此表单进行比较将忽略设置为未定义值的属性。
只有当函数共享相同的引用,而不仅仅是相同的代码时,才应认为它们是相等的,因为这不会考虑这些函数原型。因此,比较代码字符串并不能保证它们具有相同的原型对象。
这两个对象应该具有相同的原型链,而不仅仅是相同的属性。这只能通过比较两个对象的构造函数来测试跨浏览器是否严格相等。ECMAScript 5将允许使用object.getPrototypeof()测试它们的实际原型。一些Web浏览器还提供了一个执行相同操作的协议属性。上述代码的可能改进将允许在任何时候使用这些方法之一。
这里使用严格比较是最重要的,因为2不应被认为等于"2.0000",也不应被认为等于空、未定义或0。
效率方面的考虑使我尽快比较财产的平等性。然后,只有在失败的情况下,才能查找这些属性的类型。对于具有大量标量属性的大型对象,速度提升可能非常重要。
不再需要两个循环,第一个循环检查左对象的属性,第二个循环检查右对象的属性并仅验证是否存在(不是值),以捕获用未定义的值定义的这些属性。
总的来说,这个代码只处理16行代码中的大多数角情况(没有注释)。
更新(2015年8月13日)。我已经实现了一个更好的版本,因为函数值_equals()速度更快,能够正确地处理角情况,如与-0不同的NaN和0,还可以选择强制执行对象的属性顺序和循环引用测试,由100多个自动测试作为Toubkal项目测试套件的一部分提供支持。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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; }; |
比较仅一级对象的简单方法。
当然不是唯一的方法——你可以用一种方法来原型(反对这里的对象,但是我当然不会建议使用对象为实况代码)来复制C/j/java风格的比较方法。
编辑,因为似乎需要一个常规示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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()测试方法绝对不够好,但是一个可以接受的方法是非常困难的,因为空格有意义或没有意义,不要介意同义词方法和方法在不同的实现中产生相同的结果。以及针对对象的原型设计问题。
下面的算法将处理自引用数据结构、数字、字符串、日期,当然还有纯嵌套的javascript对象:
当
- 它们与
=== 完全相同(首先打开字符串和数字,以确保42 与Number(42) 相等) - 或者两者都是日期,并且具有相同的
valueOf() 。 - 或者它们都是同一类型,不为空,而且…
- 它们不是对象,每个
== 相等(捕获数/字符串/布尔值) - 或者,忽略具有
undefined 值的属性,它们具有相同的属性,所有这些属性都被认为是递归等价的。
- 它们不是对象,每个
函数文本认为函数不相同。此测试不充分,因为函数可能具有不同的闭包。只有当
避免了可能由循环数据结构引起的无限循环。当
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | 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; } } |
我为对象比较编写了这段代码,它似乎可以工作。检查断言:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | 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!==假和空!=未定义。如果您不需要这样严格的检查,请删除一个"="登录"此[P]!==x
"代码内。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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; } |
然后我用下一个对象测试了它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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;返回true
a==c应为false;返回false
C==D应为假;返回的是假
a==e应为false;返回false
f==g应为true;返回true
h==g应为false;返回false
I==J预期为真;返回为真
d==k应为false;返回false
k==l应为false;返回false
这是我的版本,这个线程中的大部分内容都是集成的(测试用例的计数相同):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | 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)); } }); |
这是我的测试用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | 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)); |
如果要显式检查方法,可以使用方法.ToSource()或方法.ToString()方法。
如果您在没有JSON库的情况下工作,这可能会帮助您:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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 |