JavaScript中的对象比较

在JavaScript中比较对象的最佳方法是什么?

示例:

var user1 = {name : "nerd", org: "dev"};var user2 = {name : "nerd", org: "dev"};var eq = user1 == user2;alert(eq); // gives false

我知道如果两个对象指向完全相同的对象,则它们相等,但是有没有办法检查它们是否具有相同的属性值?

以下方法对我有效,但这是唯一的可能性吗?

var eq = Object.toJSON(user1) == Object.toJSON(user2);alert(eq); // gives true
1339356 次浏览

当然不是唯一的方法——你可以原型化一个方法(在这里针对Object,但我当然不建议在实时代码中使用Object)来复制C#/Java风格的比较方法。

编辑,因为一个一般的例子似乎是预期的:

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()的测试方法是绝对不够好,但是可以接受的方法非常困难,因为空白是否有意义,更不用说同义词方法和使用不同实现产生相同结果的方法了。

如果要显式检查方法,可以使用method.toSource()或method.toString()方法。

不幸的是,没有完美的方法,除非您递归使用_proto_并访问所有不可枚举的属性,但这仅适用于Firefox。

所以我能做的最好的就是猜测使用场景。


1)快速和有限。

当您有简单的JSON样式对象而内部没有方法和DOM节点时有效:

 JSON.stringify(obj1) === JSON.stringify(obj2)

属性的顺序很重要,因此此方法将为以下对象返回false:

 x = {a: 1, b: 2};y = {b: 2, a: 1};

2)速度慢,更通用。

在不深入研究原型的情况下比较对象,然后递归比较属性的投影,并比较构造函数。

这几乎是正确的算法:

function deepCompare () {var i, l, leftChain, rightChain;
function compare2Objects (x, y) {var p;
// remember that NaN === NaN returns false// and isNaN(undefined) returns trueif (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 prototypesif (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 iframesif ((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 canif (!(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 loopsif (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 performancefor (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 cachedrightChain = [];
if (!compare2Objects(arguments[0], arguments[i])) {return false;}}
return true;}

已知问题(嗯,它们的优先级很低,可能你永远不会注意到它们):

  • 具有不同原型结构但投影相同的物体
  • 函数可以有相同的文本,但引用不同的闭包

测试:通过测试来自如何确定两个JavaScript对象的相等性?

如果你在没有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

我已经修改了上面的代码。对于我0!==falsenull!==未定义。如果您不需要如此严格的检查,请在代码中删除一个“=”登录“这个[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;返回true

a==c预期为假;返回为假

c==d预期为假;返回为假

a==e预期为假;返回为假

f==g预期为true;返回true

h==g预期为假;返回为假

i==j预期为true;返回true

d==k预期为假;返回为假

k==l预期为假;返回为假

我写了这段代码用于对象比较,它似乎有效。检查断言:

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;}));

这是我的版本,这个线程中的几乎所有东西都是集成的(测试用例的计数相同):

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 sringif (typeof(this) === "function" || typeof(this) === "string" || this instanceof String) {return this.toString() === obj.toString();}
// numberif (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 objif (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 arraysreturn JSON.stringify(this) === JSON.stringify(obj);} else { // anyway objectsfor (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 JSONreturn JSON.stringify(sort(this)) === JSON.stringify(sort(obj));}});

这是我的TestCase:

    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));
  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;};

比较仅一级对象的简单方法。

以下算法将处理自引用的数据结构、数字、字符串、日期,当然还有普通的嵌套javascript对象:

对象被认为是等效的

  • 它们与===完全相等(首先打开字符串和数字以确保42等价于Number(42)
  • 或者它们都是日期并且具有相同的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 nullif (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;}}

这是我的ES3注释解决方案(代码后的血腥细节):

function object_equals( 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未定义属性,对象有原型链,如果不选中,可能会导致非常不同的行为。

首先,我选择不扩展Object.prototype,主要是因为null不能成为比较的对象之一,并且我认为null应该是与另一个对象进行比较的有效对象。对于Object.prototype的扩展,其他人还注意到其他合理的担忧,即可能对其他人的代码产生副作用。

必须特别注意处理JavaScript允许将对象属性设置为未定义的可能性,即存在值设置为未定义的属性。上述解决方案验证两个对象是否具有设置为未定义的相同属性以报告相等。这只能通过使用Object.hasOwnProperty(property_name)检查属性的存在来实现。还要注意JSON.stringify()删除了设置为未定义的属性,因此使用这种形式的比较将忽略设置为值未定义的属性。

只有当函数共享相同的引用,而不仅仅是相同的代码时,才应被视为相等,因为这不会考虑这些函数原型。因此比较代码字符串并不能保证它们拥有相同的原型对象。

这两个对象应该具有相同的原型链,而不仅仅是相同的属性。这只能通过比较两个对象的构造函数进行严格相等来跨浏览器测试。ECMAScript 5允许使用Object.getOritypeOf()测试它们的实际原型。一些网络浏览器还提供__proto__属性来做同样的事情。对上述代码的可能改进将允许在可用时使用这些方法之一。

这里使用严格比较是至关重要的,因为2不应该被认为等于"2.0000",也不应该被认为等于null未定义0

效率考虑使我尽快比较属性的相等性。然后,只有在失败的情况下,才寻找这些属性的类型。对于具有大量标量属性的大型对象,速度提升可能很重要。

不再需要两个循环,第一个从左对象检查属性,第二个从右对象检查属性并仅验证存在(而不是值),以捕获这些用未定义值定义的属性。

总的来说,这段代码仅用16行代码(没有注释)处理了大多数角落情况。


更新(8/13/2015)。我实现了一个更好的版本,因为函数value_equals()更快,正确处理角落情况,如NaN和0不同于-0,可选地强制执行对象的属性顺序和测试循环引用,作为Toubkal项目测试套件的一部分,支持超过100个自动化测试