Confronto oggetti in JavaScript

Qual è il modo migliore per confrontare gli oggetti in JavaScript?

Esempio:

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

So che due oggetti sono uguali se si riferiscono allo stesso identico object , ma c’è un modo per verificare se hanno gli stessi valori degli attributi?

Il seguente modo funziona per me, ma è l’unica possibilità?

 var eq = Object.toJSON(user1) == Object.toJSON(user2); alert(eq); // gives true 

Sfortunatamente non esiste un modo perfetto, a meno che non usi _proto_ ricorsivamente e acceda a tutte le proprietà non enumerabili, ma questo funziona solo con Firefox.

Quindi il meglio che posso fare è indovinare gli scenari di utilizzo.


1) Veloce e limitato.

Funziona quando si hanno oggetti semplici in stile JSON senza metodi e nodes DOM all’interno:

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

L’ORDINE delle proprietà È IMPORTANTE, quindi questo metodo restituirà false per i seguenti oggetti:

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

2) Lento e più generico.

Confronta gli oggetti senza scavare nei prototipi, quindi confronta le proiezioni delle proprietà in modo ricorsivo e confronta anche i costruttori.

Questo è un algoritmo quasi corretto:

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

Problemi noti (beh, hanno una priorità molto bassa, probabilmente non li noterai mai):

  • oggetti con diversa struttura del prototipo ma stessa proiezione
  • le funzioni possono avere testo identico ma fare riferimento a chiusure diverse

Test: i test dei passaggi provengono da Come determinare l'uguaglianza per due oggetti JavaScript? .

Ecco la mia soluzione commentata in ES3 (dettagli gory dopo il codice):

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

Nello sviluppo di questa soluzione, ho dato uno sguardo particolare ai casi d’angolo, all’efficienza, ma cercando di fornire una soluzione semplice che funzioni, si spera con un po ‘di eleganza. JavaScript consente sia le proprietà nulle che non definite e gli oggetti hanno catene di prototipi che possono portare a comportamenti molto diversi se non controllati.

Per prima cosa ho scelto di estendere Object invece di Object.prototype , principalmente perché null non può essere uno degli oggetti del confronto e credo che null debba essere un object valido da confrontare con un altro. Ci sono anche altre preoccupazioni legittime che altri hanno notato riguardo all’estensione di Object.prototype riguardo ai possibili effetti collaterali sul codice di altri.

Si deve prestare particolare attenzione a gestire la possibilità che JavaScript consenta che le proprietà dell’object possano essere impostate su undefined , ovvero che esistano proprietà con valori non definiti . La soluzione sopra riportata verifica che entrambi gli oggetti abbiano le stesse proprietà impostate su undefined per riportare l’uguaglianza. Questo può essere ottenuto solo controllando l’esistenza di proprietà usando Object.hasOwnProperty (nome_proprietà) . Si noti inoltre che JSON.stringify () rimuove le proprietà impostate su undefined e pertanto i confronti con questo modulo ignoreranno le proprietà impostate sul valore non definito .

Le funzioni dovrebbero essere considerate uguali solo se condividono lo stesso riferimento, non solo lo stesso codice, perché questo non terrebbe conto del prototipo di queste funzioni. Quindi confrontare la stringa di codice non funziona per garantire che abbiano lo stesso object prototipo.

I due oggetti dovrebbero avere la stessa catena di prototipi , non solo le stesse proprietà. Questo può essere verificato solo attraverso il browser confrontando il costruttore di entrambi gli oggetti per l’uguaglianza rigorosa. ECMAScript 5 consentirebbe di testare il loro prototipo reale usando Object.getPrototypeOf () . Alcuni browser Web offrono anche una proprietà __proto__ che fa la stessa cosa. Un ansible miglioramento del codice sopra permetterebbe di utilizzare uno di questi metodi ogni volta che è disponibile.

L’uso di confronti rigorosi è fondamentale qui perché 2 non dovrebbe essere considerato uguale a “2.0000” , né falso dovrebbe essere considerato uguale a null , indefinito o 0 .

Considerazioni sull’efficienza mi portano a confrontare per l’uguaglianza di proprietà il prima ansible. Quindi, solo se ciò non riesce, cerca il tipo di queste proprietà. L’aumento di velocità potrebbe essere significativo su oggetti di grandi dimensioni con molte proprietà scalari.

Non sono più necessari due cicli, il primo per controllare le proprietà dall’object a sinistra, il secondo per controllare le proprietà da destra e verificare solo l’esistenza (non il valore), per catturare queste proprietà che sono definite con il valore non definito .

Nel complesso questo codice gestisce la maggior parte dei casi d’angolo in sole 16 righe di codice (senza commenti).

Aggiornamento (8/13/2015) . Ho implementato una versione migliore, poiché la funzione value_equals () è più veloce, gestisce correttamente casi d’angolo come NaN e 0 diversi da -0, opzionalmente impone l’ordine delle proprietà degli oggetti e il test per i riferimenti ciclici, supportato da oltre 100 test automatici come parte della suite di test del progetto Toubkal .

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

Un modo semplice per confrontare gli oggetti ONE-LEVEL only.

Certamente non è l’unico modo – è ansible prototipare un metodo (contro Object qui, ma certamente non suggerirei di usare Object per codice live) per replicare i metodi di confronto in stile C # / Java.

Modifica, dal momento che un esempio generale sembra essere previsto:

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

Si noti che i metodi di test con toString () non sono assolutamente sufficienti, ma un metodo che sarebbe accettabile è molto difficile a causa del problema degli spazi bianchi che hanno significato o meno, non importa sinonimo di metodi e metodi che producono lo stesso risultato con diverse implementazioni. E i problemi di prototipazione contro Object in generale.

Il seguente algoritmo si occuperà di strutture di dati autoreferenziali, numeri, stringhe, date e, naturalmente, oggetti javascript nidificati semplici:

Gli oggetti sono considerati equivalenti quando

  • Sono esattamente uguali per === (String e Number vengono prima scartati per garantire che 42 sia equivalente a Number(42) )
  • oppure sono entrambe date e hanno lo stesso valueOf()
  • o sono entrambi dello stesso tipo e non null e …
    • non sono oggetti e sono uguali per == (cattura numeri / stringhe / booleani)
    • oppure, ignorando le proprietà con un valore non undefined hanno le stesse proprietà che sono tutte considerate equivalenti ricorsivamente.

Le funzioni non sono considerate identiche dal testo della funzione. Questo test è insufficiente perché le funzioni potrebbero avere chiusure diverse. Le funzioni sono considerate uguali solo se === dice (ma potresti estendere facilmente quella relazione equivalente se dovessi scegliere di farlo).

I loop infiniti , potenzialmente causati da strutture circolari, sono evitati. Quando i tentativi areEquivalent tentano di confutare l’uguaglianza e ricorrono nelle proprietà di un object per farlo, tengono traccia degli oggetti per i quali è necessario questo confronto parziale. Se l’uguaglianza può essere smentita, allora alcuni percorsi di proprietà raggiungibili differiscono tra gli oggetti, e quindi ci deve essere un percorso raggiungibile così breve, e quel percorso raggiungibile più breve non può contenere cicli presenti in entrambi i percorsi; vale a dire che è OK assumere l’uguaglianza quando si confrontano gli oggetti in modo ricorsivo. L’ipotesi è memorizzata in una proprietà areEquivalent_Eq_91_2_34 , che viene cancellata dopo l’uso, ma se il grafico dell’object contiene già una proprietà di questo tipo, il comportamento non è definito. L’uso di tale proprietà marker è necessario perché javascript non supporta i dizionari che utilizzano oggetti arbitrari come chiavi.

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

Ho scritto questo pezzo di codice per il confronto degli oggetti, e sembra funzionare. controlla le asserzioni:

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

Ho modificato un po ‘il codice sopra. per me 0! == false e null! == indefinito . Se non hai bisogno di un controllo così rigoroso, rimuovi un ” = ” accedi a ” this [p]! == x [p] ” all’interno del codice.

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

Quindi l’ho testato con i seguenti oggetti:

 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 previsto vero; restituito vero

a == c previsto falso; restituito falso

c == d previsto falso; restituito falso

a == e previsto falso; restituito falso

f == g previsto vero; restituito vero

h == g previsto falso; restituito falso

== j aspettato vero; restituito vero

d == k previsto falso; restituito falso

k == l atteso falso; restituito falso

se si desidera verificare esplicitamente i metodi, è ansible utilizzare i metodi method.toSource () o method.toString ().

Ecco la mia versione, sono incluse molte cose da questo thread (stessi conteggi per i casi di test):

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

Ecco il mio 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)); 

Se lavori senza la libreria JSON, forse questo ti aiuterà:

 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