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

Desarrollar con cero dependencias, al menos una vez al año

La gran cantidad de utilidades, funciones, paquetes, módulos y frameworks que tenemos disponibles nos hacen la vida más sencilla y aumentan nuestra productividad, pero con el tiempo nos hacen olvidar lo más básico. De vez en cuando, atrévete a hacer un desarrollo con cero dependencias.

JSDayES – Vídeos

Si te lo has perdido o si quieres volver a ver las charlas y talleres del impresionante JSDayES 2017, aquí tienes todos los vídeos. Ponentes de primer orden, tanto nacionales como internacionales, muestran todos los aspectos de Javascript.

The CITGM Diaries by Myles Borins [video]

Myles Borins de Google, miembro del CTC de Node.js (Node.js Core Technical Committee), nos cuenta (en inglés, por supuesto) como funciona CITGM (Canary In The Gold Mine), una herramienta que permite obtener un módulo de NPM y probarlo usando una versión específica de NodeJS.

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.