Seleccionar página

Muchos elementos de Javascript son objetos. Las fechas, matrices y funciones son objetos. Podemos crear nuestros propios objetos por medio de expresiones literales ({}), funciones constructoras y new, con Object.create() y desde ES6 a partir de class. Los objetos están por todas partes en Javascript, pero nos encontramos que comparar objetos conlleva bastantes dificultades, ya que el lenguaje como tal no nos ofrece un mecanismo para comparar si dos objetos tiene las mismas propiedades.

Como continuación a nuestra serie de artículos donde vamos describiendo diferentes características de Javascript con la excusa de implementar una función universal que permita comparar cualquier tipo de datos, vamos a analizar la comparación de objetos en Javascript.

Comparación natural de objetos

De forma directa, cuando comparamos dos variables que contiene objetos, no se comparan los valores de las propiedades del objeto, se comparan sus referencias al objeto. Esto cuesta un poco de comprender: dos variables son iguales con === si apuntan exactamente al mismo objeto, no a dos objetos con iguales valores.

var obj1 = {a: 1, b: 2};
var obj2 = {a: 1, b: 2};
var obj3 = obj1;
console.assert( obj1 !== obj2 );
console.assert( obj1 === obj3 );

Cuando escribimos obj3 = obj1 lo que estamos haciendo es al asignar el objeto en otra variable, pero no se está creando un nuevo objeto, se está almacenando una referencia del objeto contenido en obj1 dentro de la variable obj3. El objeto sigue siendo el mismo, aunque ahora tiene dos variables que hacen referencia al mismo. De esta forma, si modificamos algo en obj1 ese cambio también será accesible desde obj3. De esta forma obj1 === obj3 es true ya que ambas variables apuntan exactamente al mismo objeto.

Esto complica bastante la vida del programador, ya que copiar y comparar objetos se vuelve una tarea bastante complicada. Objetos, matrices, fechas son todos objetos en Javascript y por lo tanto comparar este tipo de valores requiere de algún tipo de estrategia que podríamos considerar como no natural.

En el caso de las fechas y los objetos wrapper sobre los tipos nativos ya hemos explicado en nuestro último artículo cómo realizar su comparación sobre la base de valueOf(). Ahora nos centraremos en la forma de comparar los valores de los objetos por medio de sus propiedades.

Existen bastantes estrategias para realizar comparaciones de objetos y matrices en base a su contenido, desde utilizar JSON.stringify() para hacer una comparación de cadenas, hasta realizar recorridos por los elementos del objeto o matriz para comprobar si los valores son exactamente los mismos. Vamos a darles un repaso.

JSON.stringify() para la comparar de objeto

Lo cierto es que JSON.stringify() funciona en algunos casos y es un método que podemos ver recomendado en bastantes referencias en Internet, pero en la práctica tiene algunos problemas importantes y no es una segura y rápida forma de resolver el problema. Básicamente esta estrategia consiste en convertir los dos objetos a cadena y compararlas.

var obj1 = {a: 1, b: 2};
var obj2 = {a: 1, b: 2};
console.assert( JSON.stringify(obj1) === JSON.stringify(obj2) );

var arr1 = [1,2,3,4,5];
var arr2 = [1,2,3,4,5];
console.assert( JSON.stringify(arr2) === JSON.stringify(arr2) );

Los principales problemas de utilizar JSON.stringify() para comparar objetos son:

  • Las propiedades de los objetos no se muestra en orden alfabético, por lo que se pueden producir diferencias por el orden en el que han sido creadas.
  • Solo se incluyen las propiedades enumerables del objeto, lo cual puede ser algo limitado en algunos casos.
  • Podemos encontrarnos con objetos o tipos que pueden llegar a ser problemáticos en esta comparación en base a cadenas.
  • No funciona con objetos con referencias circulares, es decir, que dentro de un objeto se haga referencia a si mismo.
  • Es bastante lento, ya que además de serializar los dos objetos, se realizará una comparación completa de una cadena, que puede llegar a ser bastante larga

En este sencillo caso, el resultado no es el que podríamos esperar:

var obj1 = {a: 1, b: 2};
var obj2 = {b: 2, a: 1};
console.assert( JSON.stringify(obj1) === JSON.stringify(obj2) );

Simplemente cambiando el orden en el que se han creado las propiedades obtenemos cadenas diferentes (al menos en muchas implementaciones de Javascript), por lo que en la práctica esta no es una estrategia consistente y segura para comparar objetos.

Comparar analizando las propiedades del objeto

La otra estrategia para comparar el contenido de dos objetos es analizar las propiedades del objeto uno a uno y compararlas. De momento vamos a comparar sólo las propiedades propias enumerables tal y como las devuelve Object.keys(), más adelante nos cuestionaremos si estas son las únicas propiedades relevantes, pero para empezar es suficiente.

var i;
var obj1 = {a: 1, b: 2, c: true};
var obj2 = {c: true, b: 2, a: 1};
for (i in obj1) {
    console.assert(obj1[i] === obj2[i]);
}
for (i in obj2) {
    console.assert(obj1[i] === obj2[i]);
}

Para evitar este doble bucle, que necesitamos para estar seguros que no hay más propiedades en un objeto que en otro, podemos utilizar una aproximación similar, pero algo más sencilla, comparando el número de propiedades, el nombre de las mismas y sus valores.

function compareObj(a, b) {
    var aKeys = Object.keys(obj1).sort();
    var bKeys = Object.keys(obj2).sort();
    if (aKeys.length !== bKeys.length) {
        return false;
    }
    if (aKeys.join('') !== bKeys.join('')) {
        return false;
    }
    for (var i = 0; i < aKeys.length; i++) {
        if ( a[aKeys[i]]  !== b[bKeys[i]]) {
            return false;
        }
    }
    return true;
}

var obj1 = {a: 1, b: 2, c: true};
var obj2 = {c: true, b: 2, a: 1};
var obj3 = {c: false, b: 2, a: 1};

console.assert(  compareObj(obj1, obj2)  );
console.assert( !compareObj(obj1, obj3)  );

Aunque esta implementación compara correctamente los objetos, sólo hace la comparación de valores a primer nivel, es decir, si hay un objeto que contiene otro objeto, o una matriz que obtiene un objeto u otra matriz, no compararemos en profundidad el contenido.

Comparación de objetos en profundidad en equal()

Para solventar el problema de la comparación en profundidad lo que tenemos que hacer es llamar recursivamente a la función de comparación y comprobar las propiedades de cada objeto anidado. Para ello vamos implementar esta comparación recursiva en la función equal que venimos implementando en esta serie de artículos incorporando estas líneas a nuestra función.

if (aType === 'object') {
    aKeys = Object.keys(a).sort();                  // Get enumerate properties
    bKeys = Object.keys(b).sort();
    if (aKeys.length !== bKeys.length) {            // Check number of properties
        return equal.NOT_EQUAL;
    }
    if (aKeys.join('') !== bKeys.join('')) {        // Check name of properties
        return equal.NOT_EQUAL;
    }
    for (i = 0; i < aKeys.length; i++) {            // Check each property value (recursive call)
        if (!equal(a[aKeys[i]], b[bKeys[i]], options)) {
            return equal.NOT_EQUAL;
        }
    }
    if (a.constructor === b.constructor) {          // It's the same constructor and as result is the same type
        return equal.PROPERTIES_AND_TYPE;
    }
    if (options.nonStrict) {                        // Non strict comparison (optional)
        return equal.PROPERTIES;
    }
    return equal.NOT_EQUAL;                         // Not equal
}

Puedes consultar el código completo de esta versión de esequal.js en GitHub y sus pruebas.

Como en la función equal() ya comparamos cualquier otro tipo de dato, con la llamada recursiva a equal() podemos comparar sin problemas el contenido de los objetos, aunque estos contentan en sus propiedades valores con tipos primitivos o cualquier tipo de objeto.

Conclusiones

Parece sencillo comparar en Javascript, pero como estamos viendo, comparar los valores es una operación bastante compleja cuando nos enfrentamos a los objetos. La comparación natural de objetos sólo es cierta si estamos comparando exactamente la referencia al mismo objeto, pero no podemos comparar los contenidos del objeto sin entrar a analizar propiedad por propiedad.

Aunque nuestra función equal() parece ya bastante completa, lo cierto es que es muy mejorable, lo cual vamos a abordar en los próximos artículos y de esta forma nos adentraremos aún más en el mundo de los objetos en Javascript, profundizando sobre algunas de sus características menos comentadas, pero importantes. Si no puedes esperar, descarga la versión final de esqueal.js con npm.

Novedades

Explorando ArrayBuffer, DataView y matrices con tipo

Hasta hace relativamente poco en Javascript era complicado gestionar datos binarios. ArrayBuffer, DataView y las matrices con tipo (Typed Array) ponen a nuestra disposición un conjunto bastante completo de herramientas para manejar tipos binarios sin problemas. Vamos a ver cómo funcionan…

Objetos Map y Set

Los objetos Map y Set nos pueden ser de gran ayuda para gestionar conjuntos de datos, pudiendo simplificar nuestros programas en muchas circunstancias. Es interesante que sepamos cómo se utilizan y que pequeños secretos esconden. Vamos a revisarlos…

Referencia circular en objetos

Todos sabemos que los objetos pueden contener otros objetos, pero de lo que quizás no somos conscientes es que con mucha facilidad podemos crear una referencia circular, es decir, que si recorremos las propiedades del objeto y vamos profundizando, llegamos de nuevo al objeto inicial. Debemos tener en en cuenta esta circunstancia a la hora de realizar algunas operaciones o tendremos problema. Veamos cómo…

Características de las propiedades de los objetos

Existen varios tipos de propiedades que se comportan de forma diferente. Tenemos que tener en cuenta es la diferencia entre propiedades enumerables y no enumerables, propias y heredadas, de sólo lectura o no configurables, sin olvidar alguna que otra convención para definir propiedades como privadas. Veamos cómo trabajar con los distintos tipos de propiedades de un objeto.

breves

Descrubir algunas características de console

En el día a día nos encontramos muy a menudo utilizando console. Es una navaja multiusos que nos facilita la vida a la hora de depurar nuestro código. La mayoría de nosotros ha utilizado console.log(), pero tiene otras muchas funcionalidades.

Matrices dispersas o sparse arrays en JS

Una característica que puede producir algunos problemas, si no lo tenemos en cuenta, es la posibilidad de tener matrices con huecos, es decir, con algunos de sus elementos sin definir. Es lo que se suele denominar una matriz dispersa o sparse array. Veamos cómo trabajar con esta características de las matrices.

Algunos operadores de bits usados con asiduidad

Cada día más a menudo podemos encontrar operadores binarios utilizados como formas abreviadas de algunas operaciones que de otra forma sería algo menos compactas y, quizás, más comprensibles. Veamos algunos casos en detalle.

Cómo diferenciar arrow function de function

En un reciente artículo Javier Vélez Reyes hace patente las principales diferencias entre las funciones tradicionales y las funciones flecha, ya que ambos modelos no son equivalentes e intercambiables. Veamos cómo es posible saber si una función ha sido construida por medio de la instrucción function o como una arrow function.