Por amor al código Code snippets y reflexiones sobre tecnología

Comparando objetos por valor

Una de las típicas quejas de los programadores sobre javascript es que, aunque permite crear objetos de forma literal:


const foo = { bar: 'a property' }

La comparación de objetos no es por valor, si no que es referencial, es decir, dos objetos no son iguales desde el punto de vista del comparador de igualdad a no ser que sean el mismo objeto:


const a = { bar: 'a property' }
const b = { bar: 'a property' }
const c = a

c.foo = 'another property'

a === b // false
c === a // true c apunta al mismo objeto, la asignación no ha clonado el objeto

Es decir, aunque en la sintaxis pueda parecer que los objetos son valores como en otros lenguajes funcionales (LISP o Haskell por ejemplo) en realidad solo son punteros a los objetos, al estilo de lenguajes imperativos como C. Esta disonancia empeora si además usamos el operador == que hace casting de forma automática de los argumentos para realizar la comparación, con lo que obtenemos cosas como:


!!0 // false
!![] // true
!!{} // true
0 == [] // true wtf?

0 == {} // false 
{} == {} // false 


Es difícil razonar con el lenguaje haciendo transformaciones implícitas y no obvias a simple vista, pero hay una solución muy sencilla para comprarar objetos literales en javascript por valor:


const a = { bar: 'a property' }
const b = { bar: 'a property' }
JSON.stringify(a) === JSON.stringify(b) // true

JSON.stringify convertirá el objeto a una cadena, podemos usarlo con objetos anidados sin problemas mientras no haya funciones dentro de las propiedades o propiedades cíclicas. Los objetos literales nos permiten estructurar nuestros datos y acceder fácilmente a ellos, siempre han sigo muy utilizados en JavaScript y más desde el auge de las APIs JSON, pero aún más desde que llegaron React y Redux a revolucionar la comunidad y promover el uso de funciones puras, inmutabilidad y objetos literales para manejar nuestras aplicaciones.

Crear una función isEqual JSON.stringify es trivial:


function isEqual(a, b) {
  return JSON.stringify(a) === JSON.stringify(b)
}

isEqual({}, {}) // true
isEqual([], []) // true

const a = { bar: 'a property' }
const b = { bar: 'a property' }

isEqual(a, b) // true

Pero ¿y eso no será muy lento?

Es algo más lento que la implementación de Underscore, pero debemos recordar que:

Premature optimization is the root of all evil – DonaldKnuth

Desde luego, si usas alguna de estas o otra librería funcional que tenga este método: úsalo, la implementación está más que testeada y optimizada, es una apuesta segura.

Pero si no tienes esta dependencia en tu proyecto, a lo mejor no quieres añadirla.

Y a no ser que realmente vayamos a usar esta operación decenas de miles de veces por segundo de forma continuada, no nos merezce la pena el peso extra y la dependencia que añadimos a la aplicación. De todas formas, para poder comparar, enlazo a jsperf con un benchmark que aparece nada más buscar el tema en google: https://jsperf.com/lo-dash-vs-underscore-vs-json-stringify-isequal

La solución de JSON.stringify se entiende a simple vista por cualquier programador de JavaScript, no tiene ninguna lógica oculta que nos haga preguntarnos que haría en tal o cual caso y si es necesario siempre se puede sustituir más adelante por algo más sofisticado.

Si quieres comentar, corregir algo o contribuir, puedes hacerlo en twitter o en Github. Cualquier comentario, crítica o contribución será bien recibido.