viernes, 10 de agosto de 2012

Comparando objetos en javascript

La verdad es que por muchas horas que invierta en programar en javascript y en leer esos pequeños detalles que tiene y que pueden volverte loco un par de horas no paro de sorprenderme al ver algunas cosas. La última sin ir más lejos es la siguiente y la mostraré con un simple trozo de código.
var u1 = {prop1 : 1, prop2: "value2"};
var u2 = {prop1 : 1, prop2: "value2"};
console.log(u1 == u2); 
console.log(u1 === u2);
Simplemente estamos declarando dos objetos y los estamos comparando. Como vemos ambos objetos tienes las mismas propiedades (prop1 y prop2) y los mismos valores en dichas propiedades, prop1 vale 1 y prop2 vale "value2". Las siguientes lineas muestran por la consola el resultado de comparar ambos objetos. Supongo que la respuesta lógica sería pensar que algunas de las dos comparaciones debería devolver true. ¿Verdad?, pues no, ambas comparaciones devuelve false.

Si el objeto lo declaramos de otra manera menos que el efecto es el mismo
function Foo(p1,p2) {
    this.prop1 = p1;
    this.prop2 = p2;    
}

var u1 = new Foo(1, "value2");
var u2 = new Foo(1, "value2");

console.log(u1 == u2);  // false
console.log(u1 === u2); // false
Vemos que en ese caso el comportamiento similar así que podemos descartar que este comportamiento sea debido a como hemos declarado nuestros objetos.

Después de pensar un poco en el asunto y necesitando una función que me comparara dos objetos busqué la forma de hacer dicha comparación. Lo primero que determiné es que son para mi dos objetos iguales, y para mi negocio son aquellos objetos cuyas propiedades (solo propiedades y no funciones) sean iguales. O sea que esta comparación sería correcta
function Foo(p1,p2) {
    this.prop1 = p1;
    this.prop2 = p2;
    this.bar = function() {
        // do operation
    }        
}

var u1 = new Foo(1, "value2");
var u2 = new Foo(1, "value2");

console.log(u1 == u2);  // false
console.log(u1 === u2); // false
Con eso claro lo único que tenemos que hacer es recorrer las propiedades de los objetos implicados e ir comparando valores. Recorrer las propiedades de un objeto es bastante fácil pero puede darnos algún problema si no tenemos algunas cosas en cuenta. En concreto el principal problema es que dependiendo del contexto donde recorramos las propiedades del objeto podemos obtener las propiedades heredadas de dicho objeto, por lo que para evitarlo debemos usar el método hasOwnProperty, que nos asegura que esa propiedad es del objeto en cuestión. En nuestro ejemplo
function Foo(p1,p2) {
    this.prop1 = p1;
    this.prop2 = p2;
    this.func1 = function() {
        // do operation
    }        
}

var u1 = new Foo(1, "value2");
for (var p in u1)
{
  if (u1.hasOwnProperty(p))
    console.log(p);        
}
Obtendríamos esta salida
prop1
prop2
bar
Con esto claro ya podemos intentar hacer nuestro comparador de objetos. La primera versión que se nos viene a la mente es hacer algo así
function equals(p1, p2)
{
    // Comparamos los objetos
}
Esta alternativa no es mala, pero pienso que es más elegante extender el tipo object y añadir el método equals, para poder hacer algo así
p1.equals(p2);
Esto es bastante fácil en javascript y el esqueleto sería el siguiente
Object.prototype.equals = function(x)
{
  // Comparamos los objetos 
}
Al principio del método podemos hacer algunas comparaciones básicas como si x no está definido o si los objetos a comparar tienen el mismo número de propiedades. Esta útlima comparación se hace a través del método keys que desgraciadamente no está disponible en todos los navegadores (se puede consultar su disponibilidad aquí). Con esto, nuestro método ya va tomando cuerpo y sería de la siguiente forma
Object.prototype.equals = function(x)
{   
    if (x === null || x === undefined) 
      return false;
            
    if (Object.keys(this).length != Object.keys(x).length)
      return false;
       
    // Comparamos los objetos                             

    return true;                
}
Con esto ya lo único que nos queda es recorrer las propiedades del objeto de origen e ir buscando sus homólogas para luego comparar su valor. En estas comparaciones veremos de que tipo es la propiedad, para ver si comparamos directamente sus valores, volvernos a llamar al método equals en caso de ser un objeto o devolvemos false en caso que sea una función. Con esto nuestro método equals quedaría de la siguiente manera
Object.prototype.equals = function(x)
{   
    if (x === null || x === undefined) 
        return false;
            
    if (Object.keys(this).length != Object.keys(x).length) return false;
                                   
    for (var p in this)
    {
        // Evitamos navegar por las propiedades "heredadas"
        if (this.hasOwnProperty(p)) {
            // No es una propiedad de x                 
            if (!x.hasOwnProperty(p)) return false;             
            switch(typeof(this[p])) {
                case 'function': 
                    // No admitimos objetos con funciones
                    return false;
                case 'object': 
                    // Comparamos los objetos
                    if (!this[p].equals(x[p]))
                         return false;  
                    break; 
                default:             
                    // Las propiedades tienes valores distintos
                    if (this[p] !== x[p])
                        return false;
                    break;
            }                        
        }
    }

    return true;                
}
Para probar este código de manera rudimentaria he hecho este código y lo he lanzado en jsFiddle y los resultado obtenidos son los esperados
var u1= {prop1 : "value1", prop2: "value2"};
var u2= {prop1 : "value1", prop2: "value2"};
console.log('u1 equals u2: ' + u1.equals(u2));

var v1= {prop1 : "value1", prop2: "value2"};
var v2= {prop1 : "value1", prop2: "value2", prop3: "value3"};
console.log('v1 equals v2: ' + v1.equals(v2));    

var w1= {prop1 : "value1", prop2: "value2"};
var w2= {prop1 : "value1", prop2: "value3"};
console.log('w1 equals w2: ' + w1.equals(w2)); 

var x1= {prop1 : "value1", prop2: "value2"};
var x2= {prop1 : "value1", prop3: "value2"};
console.log('x1 equals x2: ' + x1.equals(x2));

var y1= {prop1 : "value1", prop2: { prop3: "value3"}};
var y2= {prop1 : "value1", prop2: { prop4: "value4"}};
console.log('y1 equals y2: ' + y1.equals(y2));

var z1= {prop1 : "value1", prop2: { prop3: "value3"}};
var z2= {prop1 : "value1", prop2: { prop3: "value3"}};
console.log('z1 equals z2: ' + z1.equals(z2));
Puedes probar este código desde aquí

Se que esta no es la manera más óptima de probar el código y para esto existen framework de javascript como Jasmine o QUnit pero esta parte la dejaré para otro post.

En definitiva hemos visto que hay que estar atentos a muchos comportamientos que tiene javascript ya que algo tan simple como una comparación de objetos puede ser una vez más una tarea complicada.

Happy coding!

No hay comentarios:

Publicar un comentario