Published on

6 Formas de comparar Objetos en Javascript

Autores

Un tiempo atras un profesor me enseño que el concepto de comparar 2 objectos identicos en javascript, mismas propiedades, mismo nombre, mismo todo, daba un valor como falso.

Por ejemplo el siguiente

const jugo1 = {
  name: 'Mango'
};
const jugo2 = {
  name: 'Mango'
};

jugo1 === jugo1; // => true
jugo1 === jugo2; // => false

jugo1 == jugo1; // => true
jugo1 == jugo2; // => false

Como nunca tuve que poner este concepto en practica no veia por que realmente profundizar. Recientemente acomplando un nuevo feature en un formulario de "estas seguro de cambiar la pagina, los datos que has modificado no se van a guardar", por fin encontrado una razon para revisar a mayor profundidad este concepto clave en javascript.

Forma 1 - Rapida pero limitante

Una forma rapida pero limitante de comparar objetos en javascript es convirtiendo el objeto a un string. Funciona cuendo tienes objectos simples de estilo JSON sin métodos y nodos DOM dentro.

// Objectos simples, misma estructura y posicion de propiedades

const obj1 = {a: 1, b: 2};
const obj2 = {a: 1, b: 2};

JSON.stringify(obj1) === JSON.stringify(obj2) //=> true

El problema es cuando el orden de los atributos varia, dando que se estaria comparando 2 string diferentes.

// obj2 tiene el propiedad b primero

const obj1 = {a: 1, b: 2};
const obj2 = {b: 2, a: 1};

JSON.stringify(obj1) === JSON.stringify(obj2) //=> false

Forma 2 - Igualdad referencial

Javascript not da 3 manera de comparar valores:

  • La manera estricta con ===
  • La manera ligera con ==
  • Object.is() function

Cuando comparamos objetos usando cualquier de las manera de arriba, la comparativa es verdad(true) solo si los valores referenciales son iguales


const hero1 = {
  name: 'Batman'
};
const hero2 = {
  name: 'Batman'
};

hero1 === hero1; // => true
hero1 === hero2; // => false

hero1 == hero1; // => true
hero1 == hero2; // => false

Object.is(hero1, hero1); // => true
Object.is(hero1, hero2); // => false

Forma 3 - Comparación manual

La manera obvia de comparar objetos es leyendo el contenido de cada uno y comparando de tal manera.

// obj2 tiene el propiedad b primero

function isHeroEqual(object1, object2) {
  return object1.name === object2.name;
}

const hero1 = {
  name: 'Batman'
};
const hero2 = {
  name: 'Batman'
};
const hero3 = {
  name: 'Joker'
};

isHeroEqual(hero1, hero2); // => true
isHeroEqual(hero1, hero3); // => false

isHeroEqual() compara cierta propiedad de cada objecto introducido para comparar.

Forma 4 - Igualdad superficial


function shallowEqual(object1, object2) {
  const keys1 = Object.keys(object1);
  const keys2 = Object.keys(object2);

  if (keys1.length !== keys2.length) {
    return false;
  }

  for (let key of keys1) {
    if (object1[key] !== object2[key]) {
      return false;
    }
  }

  return true;
}


const hero1 = {
  name: 'Batman',
  realName: 'Bruce Wayne'
};
const hero2 = {
  name: 'Batman',
  realName: 'Bruce Wayne'
};
const hero3 = {
  name: 'Joker'
};

shallowEqual(hero1, hero2); // => true
shallowEqual(hero1, hero3); // => false


const hero1 = {
  name: 'Batman',
  address: {
    city: 'Gotham'
  }
};
const hero2 = {
  name: 'Batman',
  address: {
    city: 'Gotham'
  }
};

shallowEqual(hero1, hero2); // => false

Forma 5 - Igualdad profunda


function deepEqual(object1, object2) {
  const keys1 = Object.keys(object1);
  const keys2 = Object.keys(object2);

  if (keys1.length !== keys2.length) {
    return false;
  }

  for (const key of keys1) {
    const val1 = object1[key];
    const val2 = object2[key];
    const areObjects = isObject(val1) && isObject(val2);
    if (
      areObjects && !deepEqual(val1, val2) ||
      !areObjects && val1 !== val2
    ) {
      return false;
    }
  }

  return true;
}

function isObject(object) {
  return object != null && typeof object === 'object';
}


const hero1 = {
  name: 'Batman',
  address: {
    city: 'Gotham'
  }
};
const hero2 = {
  name: 'Batman',
  address: {
    city: 'Gotham'
  }
};

deepEqual(hero1, hero2); // => true

Forma 6 - Lenta y más genérica

Compara objetos sin profundizar en prototipos, luego compara las proyecciones de propiedades de forma recursiva y también compara constructores.


function deepCompare () {
  var i, l, leftChain, rightChain;

  function compare2Objects (x, y) {
    var p;

    // recuérdalo NaN === NaN => false
    // y isNaN(undefined) => true
    if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
         return true;
    }

    // Compara primitivas y funciones.    
    // Verifica si ambos argumentos se vinculan al mismo objeto.
    // Especialmente útil en el paso donde comparamos prototypes.
    if (x === y) {
        return true;
    }

    // Funciona en caso de que las funciones se creen en el constructor.
    // Comparar fechas es un escenario común.
    // Incluso podemos manejar funciones transmitidas a través de 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();
    }

    // Por fin comprobando los prototypes lo mejor que podemos
    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;
    }

    // Verifica si hay bucles de enlace infinitivo
    if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) {
         return false;
    }

    // Comprobación rápida de que un objeto es un subconjunto de otro.
    // Mejora abierta: almacenar en caché la estructura de argumentos [0] para el rendimiento (memo)
    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; 
    // throw "Necesita dos o más argumentos para comparar";
  }

  for (i = 1, l = arguments.length; i < l; i++) {

      leftChain = []; 
      rightChain = [];

      if (!compare2Objects(arguments[0], arguments[i])) {
          return false;
      }
  }

  return true;
}

Problemas con esta solucion

  • Objetos con diferente estructura de prototype pero misma proyección.
  • las funciones pueden tener texto idéntico pero se refieren a closures diferentes

Resumiendo

En mi humilde opinion, la primera propuesta va a cumplir siempre y cuando estes atento a las limitantes. Es una opcion perfecta si estas aludiendo a cambios echo (ver is esta dirty) y tambien no introducir complejidad addicional que capaz disminuya la velocidad de lanzar al mar el projecto.