Lo que Nadie te Enseña Sobre la Igualdad en JavaScript

Tiempo
~15m

Una de las partes fundamentales de un lenguaje de programación (en mi opinión, en todos) es la capacidad de comparar dos o más valores entre ellos. Y lo admito, cuando hablamos de "operaciones de igualdad en JavaScript" tenemos una percepción negativa sobre ello y lo que automáticamente hacemos es evitarlo o ignorarlo, pero no tiene por qué seguir siendo así.

El objetivo de este artículo es cambiar tu percepción e interpretación sobre lo que muchos le llaman "lo malo de JavaScript". Describiremos en detalle qué ocurre cuando comparamos valores para ayudarte a escribir programas que tengan menos errores y sean más legibles. Aprender sobre igualdad cambió como escribo mi código y me dio más confianza en lo que hago.

¡Espero que para ti tenga un resultado similar!

Te advierto, lo que leerás en este artículo te va a sonar muy extraño, básicamente porque no es la opinión popular sobre este tema. Hay mucha gente experta en JavaScript que, personalmente, no entienden estos conceptos bien. Juzgo esto por cómo los he visto y escuchado explicarlo; otros simplemente lo ignoran y se quedan con la explicación obvia.

Yo lo único que te puedo pedir es que leas este artículo con mente abierta, y al final intentes construir tu propio criterio.

¿Por qué tengo que aprender sobre igualdad?

Cuando hablamos de operaciones de igualdad en JavaScript inevitablemente estamos hablando de "coerción de tipos" (¡sí, JavaScript tiene tipos de datos!, te los explico en este artículo). Considero que la coerción es un pilar elemental para tener fundamentos sólidos sobre el lenguaje, me atrevería a decir de cualquier otro lenguaje dinámico, pero no estoy tan seguro de eso.

En cualquier caso, entender como los tipos de los valores y variables que usamos son manipulados, los tipos de conversiones más habituales y cuando aprovecharse o evitar la coerción de tipos es clave para escribir programas bien estructurados y con menos errores. ¿Estás lista o listo?

💡

Si no te has leído el artículo sobre Tipos Primitivos te recomiendo que lo leas antes para poder tener una visión más clara sobre ellos

¿Cómo JavaScript hace la coerción de tipos?

No podemos hablar de Igualdad sin mencionar Coerción. En JavaScript existen unas funciones llamadas "funciones abstractas", que son las que se encargan de hacer conversiones entre tipos siempre que sea necesario. Se les llama "Abstractas" porque no son funciones que nosotros no podemos ejecutar o llamar, es la forma de explicar lo que el lenguaje hace internamente (puede que ni siquiera sean funciones per-sé). De momento entendamos que existen, más adelante puedo hacer otro artículo hablando de ellas en detalle. Si quieres saber más sobre ellas, puedes ver sus definiciones en la especificación aquí.

Si lo piensas bien, no hay manera de comparar nada sin que ambas cosas sean iguales o, más o menos iguales: no podemos comparar mangos con sandías (aunque ambas sean mis frutas preferidas 🍉🥭). JavaScript hace lo mismo, para que pueda aproximar dos valores, siempre los va a intentar convertir a un tipo primitivo, a menos que le digamos lo contrario.

Una de las formas más habituales de invocar la coerción de tipos es mediante operaciones de igualdad como == y ===.

La famosa lucha entre == y ===

Si alguna vez has escuchado que la diferencia entre estas dos operaciones es que uno compara los tipos de las variables y el otro no, lamento decirte que no es exactamente así...

Puedes encontrar este tweet aquí.

TweetLoading

Cada una de estas comparaciones implementa un algoritmo para determinar su resultado, el doble iguales (==) o "Loose Equality" implementa el algoritmo llamado "Abstract Equality Comparison" y el triple iguales (===) implementa el algoritmo llamado "Strict Equality Comparison".

Si leemos los pasos que ejecuta el primer algoritmo ("Abstract Equality Comparison") podemos desmantelar el mito anterior, ya que lo primero que hace ("Abstract Equality Comparison") es verificar los tipos de los operandos, y lo que es aún más interesante, si el tipo de ambos operandos es igual, == devuelve el resultado de la comparación con Triple Iguales 😅🤯.

Abstrct Equality Loading

Listado de los pasos que sigue el algoritmo "Abstract Equality Comparison". Enmarcado en rojo está el primer paso que comenta la ejecución de === si los tipos de ambos valores son iguales. La fuente de esta documentación la encuentras aquí.

Pero si esa no es la diferencia, ¿Cuál es?

Para ser más concreto en la diferencia entre ambos algoritmos o operaciones, podemos decir que el doble igual (==) permite la coerción de tipos y el triple igual (===) no lo permite.

La "comparación estricta" lo primero que hace es comparar los tipos de los operandos, y si no son iguales, directamente devuelve false sin importar nada más (puedes comprobarlo aquí). Resumiendo, ambas operaciones verifican los tipos de los operandos a comparar, la diferencia es lo que hacen cuando los tipos son distintos.

Una pregunta que te puedes hacer para ayudarte a determinar cuál de los dos usar es: ¿Cuándo compare dos valores, quiero permitir coerción o no? 🤔

Ahora bien, es verdad que Coerción es un pilar fundamental en JavaScript, pero esto no quiere decir que permitir cualquier tipo de coerción tenga sentido. Aquí es donde tenemos que procurar conocer lo más que podamos los tipos que estamos comparando. Luego veremos algunos casos en donde deberíamos evitar la coerción de tipos.

En pro de respetar tu tiempo y asumir que ya estás convencida/convencido que la coerción no es algo malo en JavaScript, vayamos más al grano y definamos una serie de reglas que te pueden ayudar a usar == sin que te sientas mal:

Reglas para usar == correctamente

Si los tipos son iguales, == se comporta como ===

Como mencionamos arriba, cuando comparamos dos valores del mismo tipo, el resultado de == y === será siempre igual, básicamente porque el algoritmo de comparación abstracta lo define.

null o undefined se tratan como iguales

Veamos un ejemplo:

En este ejemplo vemos que estamos usando el === para comparar que ninguno de ambos valores (persona1.name y persona2.name) son valores que están definidos en los objetos que estamos pasando. Como ya aprendimos, no estamos permitiendo ningún tipo de coerción en esta comparación. Te puedo asegurar que la comparación explícita de ambos tipos (null y undefined) no es muy práctica, ya que entre si la variable no tiene valor o nunca fue declarada, no hay una distinción contundente para que la comparación explicita tenga sentido o sea necesaria.

Ahora comparemos con la versión que sí permite coerción:

¿Cuál crees que es más legible? Creo que es evidente que la segunda, además de ser más pequeña, expresa un entendimiento más claro sobre cómo JavaScript trata a los tipos null y undefined. Y si esto no es tan obvio para todas las personas de tu equipo, siempre puedes agregar una línea de comentario:

Este uso de != es el menos controversial de todos y el más aceptado. Y si has usado la primera versión, no te sientas mal, porque bibliotecas de JavaScript que son descargadas millones de veces a la semana lo siguen usando 😅

Otro caso en el que puedes usar tranquilamente == es cuando usas el operador typeof:

Otro caso en el que puedes usar tranquilamente == es cuando usas el operador typeof:

El operador typeof SIEMPRE va a devolver un valor de tipo String, así que puedes usar tranquilamente los == en estos casos porque la coerción de tipos nunca pasará

Prefiere hacer coerción a números

Si vemos los puntos del 4 al 7 en el algoritmo de ==, podemos ver que prefiere convertir los valores a números siempre y cuando estemos hablando de valores de tipo String, Boolean o Number.

Un ejemplo claro en el que nos podemos aprovechar de esta preferencia es cuando trabajamos con datos que vienen de una API o de un campo de formulario (incluso cuando el campo es de tipo "number" el valor que te devuelve será de tipo "string"). ¡Felicidades! 🎉 Sólo con saber esto, ya sabes más sobre igualdad que el 99% de los programadores de JavaScript que no nos leemos la especificación 😅

Si comparamos un valor que no es de tipo primitivo, ToPrimitive

Cuando digo valores "no primitivos" me refiero a Objetos. Veamos un ejemplo que puede que lo hayas visto antes, ya que es muy famoso gracias al video WAT:

Personalmente creo que este es uno de los casos que trae más confusión. pero si nos ponemos a pensar, en qué circunstancia comparar un valor con su negación tiene algún sentido? esto (¡espero!) no debe pasar en ningún programa, ¡nunca!

El resultado de hacer este tipo de comparaciones es percibir la coerción como un error o mal diseñada simplemente porque este caso completamente sin lógica alguna tiene un resultado incoherente. De todas formas, veamos lo que realmente está pasando para que entendamos porque el resultado es el que es:

Está claro que el resultado no es el esperado, pero estamos hablando de una comparación completamente ilógica, así que no me parece que sea un argumento válido en contra de permitir la coerción en nuestros programas. Creo que este es uno de los casos en los que puedes preferir el uso del ===, porque ya vemos que al comparar valores no primitivos con valores primitivos podemos tener resultados poco esperados.

¿Cuándo debemos evitar ==?

En la última regla te puedes dar cuenta que no siempre es favorable usar ==. Hay algunos casos en los que te recomiendo no usarlo:

Cuando uno de los valores es 0, "" o " "

Si uno de los valores que vas a comparar puede llegar a ser cero (0), una cadena de caracteres vacía ("") o una cadena con espacios (" ") el resultado puede ser impredecible y poco recomendable. Hay demasiados casos especiales cuando uno de los valores que comparamos son estos, que es mejor evitarlo en estos casos.

Cuando comparamos objetos (no primitivos)

Incluso cuando ambas comparaciones funcionan igual con objetos, usar == con objetos me parece que es más riesgoso y puede llegar a convertirse en un problema, por eso es mejor evitarlo cuando sabemos que estamos tratando con cualquier tipo de objetos

Comparando con true o false

Si analizamos la comparación y ahora después de saber que coerción solo ocurre cuando ambos valores son de tipos distintos, te puedes dar cuenta que comparar directamente cualquier valor a true o false tiene menos utilidad que simplemente comparar si un valor es "truthy" o "falsy"

y esto es lo que está pasando en este caso:

Recuerda que == permite la coerción siempre que ambos valores no sean del mismo tipo. Si son del mismo tipo, el resultado es igual a si usas ===. Personalmente creo que esta manera de interpretar la coerción y entender lo que realmente pasa cuando comparamos objetos es sencilla para que cualquier desarrollador de cualquier nivel pueda entenderla. O si quieres todavía un resumen en una frase: Utiliza == para permitir la coerción entre tipos primitivos y no entre objetos.

Desafió

Ahora que ya hemos visto todo lo que necesitamos saber sobre igualdad y coerción, ¡pongámoslo en práctica!

Miremos el siguiente código, el resultado en la consola debería ser true para todos los casos:

Crédito extra

Refactoriza la función para que puedas contemplar el siguiente "caso extraño" también devuelva true:

¡La respuesta está al final del post!

Conclusión

Antes de cerrar, quiero hacer unas aclaraciones finales para poder cerrar el caso de si usar == es peor o menos recomendable que ===.

Creo que llevamos ya muchos años adoptando unas prácticas que espero te haya demostrado no son del todo correctas:

  • Recuerda que == permite la coerción siempre que ambos valores no sean del mismo tipo. Si son del mismo tipo, el resultado es igual a si usas ===.
  • Conocer con los tipos de variables con los que trabajas es mejor que no saber los tipos.
  • El tipado estático no es la única forma de saber los tipos de una variable.
  • Usar == no se trata de hacer comparaciones sin conocer los tipos, en realidad es el caso contrario. Se trata de comparar valores cuando sabemos los tipos y opcionalmente permitir coerción si lo vemos necesario
  • === es innecesario cuando los tipos de los valores que estamos comparando son iguales
  • Cuando sabes los tipos de datos, == siempre será la solución más correcta. Si no sabes los tipos, quizás tengas que reestructurar el código para que sean más obvios, puede ser indicativo que no entiendes muy bien lo que tu programa debe hacer.

Notas y Referencias

No puedo recomendar más el curso de Kyle Simpson sobre "Deep JavaScript Foundations" que está disponible en FrontendMasters. Si no te lo puedes permitir, te recomiendo ver sus libros que son totalmente GRATIS en github (está trabajando en la segunda versión, esto no invalida la primer versión que es de donde saqué la mayoría de información). El capítulo sobre coerción es ORO PURO.

He aprendido muchísimo con estas fuentes, no solo por el contenido, sino a también ser más crítico y confirmar las fuentes donde consumo información. Ahora cada vez que escucho alguno de los "gurus de JavaScript" presto mucha más atención a lo que dicen y me doy cuenta si de verdad entienden lo que están haciendo o no 😉

Respuesta al Desafió

¡Aquí tienes una manera de solucionar el desafío!

¡Escríbeme tu respuesta vía twitter!,

¡Seguro alguno de ustedes consigue solucionarlo de una mejor manera!

El objetivo de este artículo es cambiar tu percepción e interpretación sobre lo que muchos le llaman "lo malo de JavaScript". Describiremos en detalle qué ocurre cuando comparamos valores para ayudarte a escribir programas que tengan menos errores y sean más legibles. Aprender sobre igualdad cambió como escribo mi código y me dio más confianza en lo que hago.

¡Espero que para ti tenga un resultado similar!

Lo que nadie te enseña sobre la Igualdad en JavaScript.