propios - operaciones con objetos javascript
Comparación de objetos en JavaScript (10)
Aquí está mi solución comentada en ES3 (detalles sangrientos después del código):
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;
}
Al desarrollar esta solución, eché un vistazo en particular a los casos de esquina, a la eficiencia, pero tratando de encontrar una solución simple que funcione, con suerte con algo de elegancia. JavaScript permite que las propiedades, tanto nulas como indefinidas , tengan cadenas de prototipos que pueden conducir a comportamientos muy diferentes si no se verifican.
Primero, elegí extender Object en lugar de Object.prototype , principalmente porque null no podía ser uno de los objetos de la comparación y creo que null debería ser un objeto válido para comparar con otro. También hay otras preocupaciones legítimas señaladas por otros con respecto a la extensión de Object.prototype con respecto a los posibles efectos secundarios en el código de otros.
Se debe tener especial cuidado para tratar la posibilidad de que JavaScript permita que las propiedades de los objetos se puedan establecer como indefinidas , es decir, existen propiedades cuyos valores se configuran como no definidos . La solución anterior verifica que ambos objetos tienen las mismas propiedades configuradas como no definidas para informar la igualdad. Esto solo puede lograrse verificando la existencia de propiedades usando Object.hasOwnProperty (property_name) . También tenga en cuenta que JSON.stringify () elimina las propiedades que están configuradas como indefinidas , y que, por lo tanto, las comparaciones que usan este formulario ignorarán las propiedades establecidas en el valor indefinido .
Las funciones deben considerarse iguales solo si comparten la misma referencia, no solo el mismo código, ya que esto no tomaría en cuenta el prototipo de estas funciones. Entonces, comparar la cadena de código no funciona para garantizar que tengan el mismo objeto prototipo.
Los dos objetos deben tener la misma cadena de prototipo , no solo las mismas propiedades. Esto solo se puede probar en el navegador cruzado comparando el constructor de ambos objetos para una igualdad estricta. ECMAScript 5 permitiría probar su prototipo real utilizando Object.getPrototypeOf () . Algunos navegadores web también ofrecen una propiedad __proto__ que hace lo mismo. Una posible mejora del código anterior permitiría utilizar uno de estos métodos siempre que esté disponible.
El uso de comparaciones estrictas es primordial aquí porque 2 no debe considerarse igual a "2.0000" , ni falso debe considerarse igual a nulo , indefinido o 0 .
Las consideraciones de eficiencia me llevan a comparar la igualdad de propiedades lo antes posible. Entonces, solo si eso falló, busque el tipo de estas propiedades. El aumento de velocidad podría ser significativo en objetos grandes con muchas propiedades escalares.
No se requieren más de dos bucles, el primero para verificar las propiedades del objeto izquierdo, el segundo para verificar las propiedades desde la derecha y verificar solo la existencia (no el valor), para capturar estas propiedades que se definen con el valor indefinido .
En general, este código maneja la mayoría de los casos de esquina en solo 16 líneas de código (sin comentarios).
Actualización (8/13/2015) . He implementado una versión mejor, ya que la función value_equals() que es más rápida, maneja adecuadamente los casos de esquina como NaN y 0 diferente a -0, aplicando opcionalmente el orden de las propiedades de los objetos y probando referencias cíclicas, respaldado por más de 100 pruebas automatizadas como parte de la suite de prueba del proyecto Toubkal .
Esta pregunta ya tiene una respuesta aquí:
¿Cuál es la mejor manera de comparar objetos en JavaScript?
Ejemplo:
var user1 = {name : "nerd", org: "dev"};
var user2 = {name : "nerd", org: "dev"};
var eq = user1 == user2;
alert(eq); // gives false
Sé que dos objetos son iguales si se refieren al mismo objeto exacto , pero ¿hay alguna forma de verificar si tienen los mismos valores de atributos?
La siguiente forma funciona para mí, pero ¿es la única posibilidad?
var eq = Object.toJSON(user1) == Object.toJSON(user2);
alert(eq); // gives true
Aquí está mi versión, casi todas las cosas de este hilo están integradas (las mismas cuentas para los casos de prueba):
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));
}
});
Aquí está mi 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));
Ciertamente no es la única forma: podría crear un prototipo de un método (contra Objeto aquí, pero ciertamente no sugeriría usar Objeto para código en vivo) para replicar los métodos de comparación de estilo C # / Java.
Editar, ya que parece esperarse un ejemplo general:
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;
}
Tenga en cuenta que probar métodos con toString () no es suficientemente bueno, pero un método que sería aceptable es muy difícil debido a que el espacio en blanco tiene significado o no, no importa los métodos de sinónimo y los métodos que producen el mismo resultado con diferentes implementaciones. Y los problemas de prototipado contra Objeto en general.
Desafortunadamente, no hay una forma perfecta, a menos que use _proto_
recursivamente y acceda a todas las propiedades no enumerables, pero esto solo funciona en Firefox.
Así que lo mejor que puedo hacer es adivinar los escenarios de uso.
1) Rápido y limitado.
Funciona cuando tiene objetos simples de estilo JSON sin métodos y nodos DOM dentro:
JSON.stringify(obj1) === JSON.stringify(obj2)
La ORDEN de las propiedades ES IMPORTANTE, por lo que este método devolverá false para los siguientes objetos:
x = {a: 1, b: 2};
y = {b: 2, a: 1};
2) Lento y más genérico.
Compara objetos sin excavar en prototipos, luego compara las proyecciones de propiedades de forma recursiva, y también compara constructores.
Este es el algoritmo casi correcto:
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;
}
Problemas conocidos (bueno, tienen una prioridad muy baja, probablemente nunca los notará):
- Objetos con estructura prototipo diferente pero misma proyección.
- Las funciones pueden tener texto idéntico pero se refieren a diferentes cierres.
Pruebas: las pruebas pasadas son de ¿Cómo determinar la igualdad para dos objetos JavaScript? .
El siguiente algoritmo se ocupará de las estructuras de datos, números, cadenas, fechas y, por supuesto, objetos de JavaScript anidados sin formato:
Los objetos se consideran equivalentes cuando
- Son exactamente iguales por
===
(Cadena y Número se desenvuelven primero para garantizar que42
es equivalente aNumber(42)
) - o ambas son fechas y tienen el mismo
valueOf()
- o ambos son del mismo tipo y no son nulos y ...
- no son objetos y son iguales por
==
(números de captura / cadenas / booleanos) - o, ignorando las propiedades con
undefined
valorundefined
, tienen las mismas propiedades, todas las cuales se consideran recursivamente equivalentes.
- no son objetos y son iguales por
Las funciones no son consideradas idénticas por el texto de la función. Esta prueba es insuficiente porque las funciones pueden tener diferentes cierres. Las funciones solo se consideran iguales si ===
dice (pero podría extender fácilmente esa relación equivalente si lo desea).
Se evitan los bucles infinitos , potencialmente causados por estructuras de datos circulares. Cuando areEquivalent
intenta refutar la igualdad y vuelve a aparecer en las propiedades de un objeto para hacerlo, realiza un seguimiento de los objetos para los que se necesita esta subcomparación. Si la igualdad puede ser refutada, entonces alguna ruta de propiedad alcanzable difiere entre los objetos, y entonces debe haber una ruta accesible más corta, y esa ruta accesible más corta no puede contener ciclos presentes en ambas rutas; es decir, está bien asumir la igualdad cuando se comparan objetos recursivamente. El supuesto se almacena en una propiedad areEquivalent_Eq_91_2_34
, que se elimina después de su uso, pero si el gráfico del objeto ya contiene dicha propiedad, el comportamiento no está definido. El uso de tal propiedad de marcador es necesario porque javascript no admite diccionarios que usan objetos arbitrarios como claves.
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;
}
}
Escribí este fragmento de código para comparar objetos, y parece funcionar. Compruebe las aserciones:
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;}));
He modificado un poco el código anterior. para mí 0! == falso y nulo! == indefinido . Si no necesita una comprobación tan estricta, elimine una " = " inicie sesión en " este [p]! == x [p] " dentro del código.
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;
}
Luego lo he probado con los siguientes objetos:
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 esperado verdadero; devuelto verdadero
a == c esperado falso; devuelto falso
c == d espera falso; devuelto falso
a == e espera falso; devuelto falso
f == g esperado verdadero; devuelto verdadero
h == g esperado falso; devuelto falso
i == j esperado verdadero; devuelto verdadero
d == k espera falso; devuelto falso
k == l esperaba falso; devuelto falso
Si desea verificar los métodos explícitamente, puede usar los métodos method.toSource () o method.toString ().
Si trabajas sin la biblioteca JSON, quizás esto te ayude:
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
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;
};
Una forma sencilla de comparar objetos de un solo nivel.