在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
不幸的是没有完美的方法,除非你recursion地使用_proto_
并且访问所有非枚举属性,但是这只能在Firefox中使用。
所以我能做的最好的是猜测使用情况。
1)快速和有限。
当你有没有方法和DOM节点的简单的JSON风格的对象时工作:
JSON.stringify(obj1) === JSON.stringify(obj2)
属性的ORDER是重要的,所以这个方法将返回false为以下对象:
x = {a: 1, b: 2}; y = {b: 2, a: 1};
2)慢,更通用。
比较对象而不挖掘原型,然后recursion比较属性的投影,并比较构造函数。
这是几乎正确的algorithm:
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; }
已知的问题(当然,他们有很低的优先级,可能你永远不会注意到他们):
- 具有不同原型结构但投影相同的物体
- 函数可能具有相同的文本,但引用不同的闭包
testing:通过testing是从如何确定两个JavaScript对象的平等? 。
这里是我在ES3中的评论解决scheme(代码后的细节):
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; }
在开发这个解决scheme时,我特别关注了angular落案例,效率问题,但是试图find一个简单的解决scheme,希望能够带来一些优雅。 JavaScript允许null和未定义的属性和对象有原型链 ,如果不检查,可能导致非常不同的行为。
首先,我select扩展Object而不是Object.prototype ,主要是因为null不能作为比较的对象之一,我相信null应该是一个有效的对象与另一个比较。 其他人也有其他关于Object.prototype关于其他代码可能产生的副作用的扩展的合理关注。
必须特别小心处理JavaScript允许将对象属性设置为未定义的可能性,即存在将值设置为未定义的属性。 上述解决schemevalidation两个对象具有相同的属性设置为undefined报告相等。 这只能通过使用Object.hasOwnProperty(property_name)检查属性的存在来完成。 另请注意, JSON.stringify()删除设置为未定义的属性,因此使用此表单进行比较将忽略设置为未定义值的属性。
只有当它们共享相同的引用时,function应该被认为是相等的,而不仅仅是相同的代码,因为这不会考虑这些function原型。 因此,比较代码string不能保证它们具有相同的原型对象。
这两个对象应该有相同的原型链 ,而不仅仅是相同的属性。 这只能通过比较两个对象的构造函数进行严格的相等来跨浏览器进行testing。 ECMAScript 5允许使用Object.getPrototypeOf()来testing它们的实际原型。 一些networking浏览器也提供了一个__proto__属性来做同样的事情。 上述代码的一个可能的改进将允许使用这些方法中的一种,只要有可用的。
严格比较的使用在这里是极为重要的,因为2不应该被认为等于“2.0000” ,也不应该认为等于null , undefined或者0 。
效率的考虑使我尽快比较财产的平等。 那么,只有在失败的情况下,才能查找这些属性的types。 对于具有许多标量属性的大对象,速度提升可能是显着的。
没有更多的两个循环是必需的,第一个检查左侧对象的属性,第二个检查右侧的属性,只validation是否存在(不是值),以捕获这些属性定义的未定义的值。
总的来说,这段代码只处理16行代码(没有注释)。
更新(8/13/2015) 。 我已经实现了一个更好的版本,因为函数value_equals()更快,可以正确处理NaN和0等不同于-0的边angular情况,可以强制执行对象的属性顺序和testing循环引用,并支持超过100个自动化testing作为Toubkal项目testing套件的一部分。
当然不是唯一的方法 – 你可以创build一个方法(针对对象在这里,但我当然不会build议使用对象的实时代码)来复制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()的testing方法绝对不够好,但是由于空白有意义的问题,一个可接受的方法是非常困难的,不用介意在不同实现中产生相同结果的同义方法和方法。 一般来说,针对Object的原型的问题。
下面的algorithm将处理自引用数据结构,数字,string,date,当然还有纯嵌套的JavaScript对象:
对象被认为是等同的
- 他们是完全相等的每个
===
(string和数字先解开,以确保42
相当于Number(42)
) - 或者它们都是date并且具有相同的
valueOf()
- 或者它们都是相同的types,而不是空的,…
- 它们不是对象,每个
==
都相等(捕获数字/string/布尔值) - 或者忽略具有
undefined
值的属性,它们具有相同的属性,所有这些属性都被认为是recursion等价的。
- 它们不是对象,每个
function文本不认为function相同。 这个testing是不够的,因为函数可能有不同的closures。 如果===
这样的话,函数只会被认为是相等的(但是如果你愿意的话,你可以很容易地扩展这个等价的关系)。
无限循环 ,可能由圆形数据结构造成,可以避免。 当areEquivalent
试图反驳相等并recursion到对象的属性中时,它会跟踪需要进行这种子比较的对象。 如果相等可以被反驳,那么一些可达的属性path在对象之间是不同的,然后必须有最短的可达path,并且最短可达path不能包含在两个path中存在的循环; 即recursion比较对象时可以假设相等。 假设存储在属性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; //eg 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;}));
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; };
简单的方法来比较只有一级的对象。
我已经修改了一下上面的代码。 对我来说0!== false和null!== undefined 。 如果您不需要这样严格的检查,请在代码中删除一个“ = ”符号“ 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; }
然后我用下一个对象进行了testing:
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预期是正确的; 回复真实
a == c预期为假; 返回false
c == d预期为false; 返回false
a == e预期错误; 返回false
f == g应该是真的; 回复真实
h == g预计错误; 返回false
我== j预期真实; 回复真实
d == k预期为false; 返回false
k == l预期错误; 返回false
如果你想明确地检查方法,你可以使用method.toSource()或method.toString()方法。
这里是我的版本,从这个线程集成了很多东西(testing用例相同):
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)); } });
这里是我的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));
如果你工作没有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