Explicación de la igualdad en JavaScript: ===, ==, operadores relacionales y Object.is
Aprenda cómo la igualdad estricta y laxa, las comparaciones relacionales y Object.is deciden realmente la identidad en JavaScript, además de las trampas de coerción que cada uno oculta.
Fuentes: Day 4 of Learning JavaScript: The Three Ways JavaScript Asks “Are These the Same Thing?”
Imagínese un formulario en el que un usuario escribe su edad como el texto "18" y una verificación contra el número 18 pasa silenciosamente, solo para que más tarde las operaciones aritméticas con ese valor den error. O una verificación de acceso que tiene éxito cuando no debería, porque dos valores diferentes resultaron convertirse en lo mismo. Los errores como estos provienen de un hecho: JavaScript tiene varias formas de determinar si dos valores son iguales, y cada una define “igual” de manera diferente. Después de leer esto, sabrá qué hacen realmente ===, ==, los operadores relacionales y Object.is(), y cuál utilizar en cada situación.
Por qué el lenguaje tiene más de una noción de igualdad
El JavaScript inicial funcionaba en navegadores donde casi toda la información ingresada provenía como texto de un formulario. En lugar de generar un error cada vez que un número aparecía como cadena, el lenguaje fue diseñado para ser tolerante: adivinaba lo que se pretendía y convertía los tipos automáticamente. Esa comodidad se convirtió en una de las fuentes más frecuentes de errores en el lenguaje, y los operadores más estrictos existen principalmente para evitarlo.
Igualdad estricta (===) como valor por defecto
Una verificación de tipo seguida de una verificación de valor
La igualdad estricta realiza una prueba en dos pasos sin conversiones ocultas. Primero compara los tipos de los dos operandos. Si difieren, el resultado es false sin que se examinen los valores. Solo cuando los tipos coinciden se comparan los valores.
console.log(1 === 1);
// true (both are numbers, both are 1)
console.log(1 === "1");
// false (the types fail immediately: number versus string)
console.log(0 === false);
// false (number versus boolean)
console.log("" === false);
// false (string versus boolean)
console.log(null === null);
// true
console.log(null === undefined);
// false (different types)
Cada false en ese fragmento proviene de la comparación entre tipos: número contra cadena, número contra booleano, cadena contra booleano, null contra undefined.
Las comparaciones de cadenas son exactas hasta el nivel del carácter. La mayúscula y minúscula son importantes, al igual que los espacios invisibles, lo cual es un error muy común en la práctica cuando alguien pulsa la barra espaciadora después de escribir una dirección de correo electrónico.
console.log("hello" === "Hello");
// false (different capitalization)
console.log("john@email.com" === "john@email.com ");
// false (the second string has a hidden space at the end)
En la práctica, esto significa normalizar la entrada del usuario, generalmente con trim() y a menudo con toLowerCase() para campos insensibles a mayúsculas y minúsculas, antes de compararla.
Dos excepciones intencionadas del estándar IEEE 754
La igualdad estricta es fiable casi en todas partes, con dos excepciones numéricas. Ninguna de ellas es un error; ambas siguen el estándar de punto flotante IEEE 754 en el que se basan los números de JavaScript. La primera es que NaN, el valor “no es un número”, no es igual a nada, ni siquiera a sí mismo.
const result = 0 / 0;
// result is NaN
console.log(result === NaN);
// false
console.log(NaN === NaN);
// false
Dado que una comprobación de igualdad nunca puede detectarlo, utilice en su lugar la función dedicada Number.isNaN().
console.log(Number.isNaN(NaN));
// true
Preferirla sobre la antigua función global isNaN(), que primero convierte su argumento en número y, por lo tanto, devuelve true para cadenas como "abc". La segunda excepción concierne al cero. JavaScript representa el cero positivo y negativo por separado, lo cual es importante en algunos códigos numéricos, pero la igualdad estricta trata ambos como iguales.
console.log(+0 === -0);
// true
Los objetos se comparan por identidad
Los primitivos como los números y las cadenas se comparan por su valor. Los objetos son diferentes: una variable que almacena un objeto en realidad contiene una referencia al lugar donde ese objeto se encuentra en la memoria. Imagina dos casas construidas con los mismos planos y pintadas del mismo color en calles distintas. Se ven idénticas, pero no son la misma casa, porque sus direcciones difieren.
Cuando compara objetos con ===, JavaScript verifica si ambos lados hacen referencia al mismo objeto e ignora su contenido interno. Las matrices siguen la misma regla, ya que también son objetos.
const userA = { name: "Alex" };
const userB = { name: "Alex" };
console.log(userA === userB);
// false (two different objects sitting in different memory slots)
console.log([] === []);
// false (two different arrays sitting in different memory slots)
Si se asigna una variable desde otra, ambas hacen referencia al mismo objeto, y la comparación tiene éxito.
const original = { name: "Alex" };
const pointer = original;
console.log(original === pointer);
// true (both point to the exact same memory location)
Para verificar si dos objetos distintos contienen los mismos datos, compare usted mismo sus propiedades primitivas.
console.log(userA.name === userB.name);
// true (now we are comparing two strings, not two object addresses)
En las estructuras anidadas, esto se convierte en una comparación recursiva, por lo que los equipos suelen recurrir a un ayudante de igualdad profunda ya probado proveniente de una biblioteca de utilidades o de su framework de pruebas, en lugar de escribir uno específico para cada caso.
Inequaldad estricta (!==)
Verificar que los valores difieren es tan común como verificar que coinciden. La inequality estricta es la negación exacta de ===: devuelve true cuando difieren tanto los tipos como los valores.
console.log(1 !== 2);
// true (they are different numbers)
console.log(1 !== "1");
// true (one is a number, one is a string)
console.log("apple" !== "orange");
// true
Es la opción adecuada siempre que el código debe ejecutarse solo si no se cumple una condición, como en las verificaciones de permisos o al filtrar elementos no deseados.
const currentRole = "guest";
if (currentRole !== "admin") {
console.log("Access denied. Admins only.");
}
// "Access denied. Admins only."
Igualdad laxa (==) y sus conversiones
Cómo la coerción produce resultados inesperados
Antes de que la igualdad estricta se convirtiera en la norma, la mayoría del código utilizaba ==. Cuando los tipos de los operandos difieren, la igualdad laxa convierte uno o ambos valores a un tipo común y luego los compara. Los resultados rápidamente dejan de ser intuitivos.
console.log(1 == "1");
// true (the string "1" becomes the number 1)
console.log(0 == false);
// true (false becomes 0)
console.log("" == false);
// true (empty string and false both become 0)
console.log(0 == "");
// true (empty string becomes 0)
console.log([] == "");
// true (empty array becomes empty string)
console.log([1] == 1);
// true (array becomes "1", which becomes 1)
Un ejemplo famoso muestra cuántos pasos ocultos pueden estar involucrados.
console.log([] == 0);
// true
Para obtener true, el motor primero convierte el array vacío en un tipo primitivo, lo que da como resultado la cadena vacía "". Los tipos siguen siendo diferentes, ya que se está comparando una cadena con un número, por lo que la cadena se convierte en un número, resultando en 0. La comparación final es 0 == 0, que equivale a true.
Los valores booleanos son igualmente complicados, porque al compararse con un valor no booleano se convierten en números antes de que ocurra cualquier otra cosa. true se convierte en 1.
console.log(true == "true");
// false
La cadena "true" no se convierte en 1; al ser convertida a número resulta en NaN, y 1 no es igual a NaN. Por esta razón, value == true casi nunca es lo que se desea.
null y undefined siguen una regla especial: bajo la igualdad laxa se consideran iguales entre sí y a nada más, ni siquiera a 0, false o la cadena vacía.
console.log(null == undefined);
// true
console.log(null == 0);
// false
console.log(null == false);
// false
console.log(null == "");
// false
console.log(undefined == 0);
// false
La verificación de null donde == es aceptable
Es mejor evitar la igualdad laxa en el código moderno, con una excepción muy utilizada. Al manejar datos de una API, a menudo se desea saber si un valor es null o undefined, y == null responde a eso con una sola comparación.
function checkStatus(status) {
if (status == null) {
console.log("Status is missing entirely");
} else {
console.log("Status is:", status);
}
}
checkStatus(null);
// "Status is missing entirely"
checkStatus(undefined);
// "Status is missing entirely"
checkStatus(0);
// "Status is: 0"
Esto funciona precisamente gracias a la regla anterior: null es igual de forma laxa únicamente a null y undefined; por lo tanto, valores falsos pero significativos como 0 o "" pasan sin ser afectados. Reemplaza a la expresión más larga status === null || status === undefined. Tenga en cuenta que muchos equipos configuran los linters para prohibir por completo el uso de ==, en cuyo caso se debe utilizar la versión estricta explícita. Por ejemplo, la regla eqeqeq de ESLint cuenta con una opción que permite exactamente esta comparación con null mientras prohíbe otros usos. En muchos casos, el operador de coalescencia nulo ?? o la cadena opcional ?. expresan la intención de manera aún más directa.
Inequaldad laxa (!=)
!= aplica las mismas conversiones que == y luego niega el resultado, por lo que hereda todas las sorpresas.
console.log(1 != "1");
// false (it converts "1" to a number, decides they are the same, so they are not different)
Aquí "1" se convierte en el número 1; los valores se consideran iguales, y por lo tanto la comprobación de “no igual” devuelve false. Trate != con la misma desconfianza que == y opte por !==.
Comparaciones relacionales: <, >, <= y >=
Números y tipos mixtos
Con dos números, los operadores relacionales se comportan exactamente como lo hacen en aritmética.
console.log(5 > 3);
// true
console.log(10 <= 10);
// true
Cuando una cadena se encuentra con un número, la cadena se convierte primero en número. Una cadena que no representa un número se convierte en NaN, y cualquier comparación relacional que involucre NaN da como resultado false.
console.log("10" > 5);
// true
console.log("abc" > 3);
// false ("abc" becomes NaN, and NaN comparisons always return false)
Dónde divergen null y undefined
Los operadores relacionales revelan una inconsistencia con la igualdad laxa. == se niega a convertir null en un número, pero <, >, <= y >= sí lo convierten en 0.
console.log(null == 0);
// false
console.log(null > 0);
// false (0 is not greater than 0)
console.log(null >= 0);
// true (0 is greater than or equal to 0)
Por lo tanto, null no es ni igual a 0 ni mayor que 0, pero sí es mayor o igual que 0. El último resultado tiene sentido una vez que se sabe que a >= b se evalúa como “a < b no es verdadero”, y 0 < 0 es falso.
undefined se comporta de manera diferente nuevamente. Al convertirse en un número, se convierte en NaN, por lo que toda comparación relacional que involucre undefined da como resultado false.
console.log(undefined > 0);
// false
console.log(undefined < 0);
// false
Las cadenas se comparan por unidad de código, no por orden del diccionario
Cuando ambos operandos son cadenas, no se realiza ninguna conversión numérica. Las cadenas se comparan carácter por carácter desde la izquierda, utilizando la unidad de código numérica UTF-16 de cada carácter.
console.log("apple" < "banana");
// true (a comes before b)
console.log("b" > "a");
// true (b comes after a)
Las letras mayúsculas tienen unidades de código más bajas que las minúsculas, lo cual explica por qué un ordenamiento simple puede colocar a “Zebra” antes que a “apple”.
console.log("Z" < "a");
// true (Z=90, a=97 in Unicode)
Para un ordenamiento orientado al usuario, utilice localeCompare() o Intl.Collator, que siguen las reglas del idioma en lugar de las unidades de código brutas.
La trampa más peligrosa involucra cadenas que contienen dígitos. Al compararse como cadenas, “10” es menor que “9”.
console.log("10" < "9");
// true (the character "1" is smaller than "9")
JavaScript no tiene idea de que estas cadenas representan cantidades. Examina el primer carácter de cada una, "1" frente a "9", descubre que "1" aparece primero y se detiene allí con un resultado de true. El mismo efecto ocurre cuando se ordena un array de números con sort() sin proporcionar un comparador, ya que el ordenamiento por defecto convierte los elementos en cadenas antes de compararlos; para realizar un ordenamiento numérico, se debe pasar (a, b) => a - b.
La solución es convertir explícitamente las cadenas numéricas en números antes de comparar sus valores.
console.log(Number("10") < Number("9"));
// false (correct: 10 is not less than 9)
Object.is() para igualdad exacta
Cerrando las lagunas de ===
Object.is() es la comprobación de igualdad más precisa que ofrece JavaScript. Para valores ordinarios, da los mismos resultados que la igualdad estricta.
console.log(Object.is(1, 1));
// true
console.log(Object.is(1, "1"));
// false
La diferencia radica exactamente en los dos casos de IEEE 754. Object.is() trata a NaN como idéntico a sí mismo:
console.log(Object.is(NaN, NaN));
// true
y distingue entre cero positivo y cero negativo:
console.log(Object.is(0, -0));
// false
Las tres comprobaciones una al lado de la otra
1y"1":==devuelvetrue;===yObject.is()devuelvenfalse.0yfalse:==devuelvetrue;===yObject.is()devuelvenfalse.nullyundefined:==devuelvetrue;===yObject.is()devuelvenfalse.
NaN y NaN: == y === devuelven false; Object.is() devuelve true.+0 y -0: == y === devuelven true; Object.is() devuelve false.[] y [], o {} y {}: los tres devuelven false, ya que se trata de objetos separados.5 y 5: los tres devuelven true.Rara vez llamarás directamente a Object.is(), pero es importante en las herramientas que utilizas a diario. React depende de Object.is() para determinar si el estado o una dependencia ha cambiado y se necesita volver a renderizar, y también por eso mutar un objeto en su lugar no dispara una actualización: la referencia sigue siendo idéntica. Las bibliotecas de estado aplican la misma lógica; consulta cómo React-Redux utiliza comprobaciones de igualdad para decidir si volver a renderizar para obtener más detalles.
Puntos clave
En el escenario inicial, donde una comprobación que debería haber denegado el acceso lo permitió, no se necesita nada más complejo que == para comparar una cadena con un número: una conversión silenciosa hace que dos valores diferentes parezcan iguales. Las reglas que lo impiden son sencillas:
- Use
===y!==por defecto, en todas partes. - Evite
==y!=, excepto el atajo intencionalvalue == nullsi sus reglas de lint lo permiten. - Detecte
NaNconNumber.isNaN(), nunca con un operador de igualdad. - Tenga en cuenta que las comparaciones de objetos verifican la identidad, no el contenido.
- Convierta cadenas numéricas con
Number()antes de usar<o>, y proporcione asort()un comparador para números. - Utilice
Object.is()cuando sea necesario tratar exactamenteNaNo cero con signo.
Las conversiones tolerantes de JavaScript no desaparecerán, pero adherirse a comparaciones estrictas evita que afecten el comportamiento en producción.
Lecturas relacionadas
- Fundamentos de TypeScript: Desde la primera anotación hasta Generics y Modo Estricto — Una guía estructurada del sistema de tipos de TypeScript, desde primitivos e inferencia hasta uniones discriminadas, Generics, tipos útiles y un tsconfig estricto.
- Fundamentos de los bucles en TypeScript: while, do-while, for, break y continue — Aprenda cómo los bucles while, do-while y for de TypeScript evalúan sus condiciones, por qué el ámbito de las variables de bucle cambia los resultados, y cómo break y continue alteran el flujo.