Comparadores de igualdad e identidad

Existen cuatro algoritmos de igualdad en ES2015:

  • Comparación de Igualdad Abstracta (==)
  • Comparación de Igualdad Estricta (===): utilizada por Array.prototype.indexOf, Array.prototype.lastIndexOf, y case-matching
  • SameValueZero: utilizado por los constructores de %TypedArray% y ArrayBuffer, así como por las operaciones Map y Set, y también por String.prototype.includes y Array.prototype.includes desde ES2016
  • SameValue: utilizado en el resto de los casos

JavaScript proporciona tres operaciones distintas para comparar la igualdad de dos elementos:

  • === (en-US) - Igualdad estricta (o "triple igual" o "identidad")
  • == (en-US) - igualdad débil o relajada ("doble igual")
  • Object.is proporciona SameValue (nuevo en ES2015).

Elegir una u otra operación depende del tipo de comparación que se esté buscando realizar.

Explicándolo de manera concisa, un doble igual lleva a cabo una conversión de tipo (cast) cuando se comparan dos cosas; el triple igual hace lo mismo pero sin realizar ninguna conversión de tipo (cast) (simplemente devuelve falso si los tipos de los elementos a comparar son diferentes); Y Object.is funciona de la misma manera que el triple igual pero hace una gestión especial de NaN, -0 y +0 de tal manera que los dos últimos no son iguales mientras que Object.is(NaN, NaN) nos devolverá true. (Si comparamos NaN con NaN de manera ordinaria , por ejemplo usando el doble igual o el triple igual, nos devolverá false, ya que la especificación IEEE 754 así lo dice). Hay que darse cuenta de que la distinción entre todas estas posibilidades tiene que ver con como se manejan los tipos primitivos; ninguna de ellas compara si los parámetros son similares en relación a su estructura. Para comparar dos objectos no primitivos , x e y, que tengan la misma estructura pero que sean objetos diferentes entre ellos, todos los casos anteriormente descritos devolverán false.

Igualdad Estricta usando ===

El operador igualdad estricta compara la igualdad de dos valores. Ninguno de estos valores se convierte de manera implícita antes de ser comparado. Si los valores tienen tipos diferentes son considerados diferentes. Por el contrario, si los valores tienen el mismo tipo y no son números, son considerados iguales si tienen el mismo valor. Finalmente, si ambos valores son números, son considerados iguales si ambos no son NaN y tienen el mismo valor, o si uno es +0 y otro -0.

js
var num = 0;
var obj = new String("0");
var str = "0";
var b = false;

console.log(num === num); // true
console.log(obj === obj); // true
console.log(str === str); // true

console.log(num === obj); // false
console.log(num === str); // false
console.log(obj === str); // false
console.log(null === undefined); // false
console.log(obj === null); // false
console.log(obj === undefined); // false

La igualdad estricta es casi siempre el operador igualdad más adecuado. Para todos los valores, excepto para los números, utiliza la semántica obvia: un valor sólo es igual así mismo. Para número usa una semántica ligeramente diferente para paliar dos casos límites diferentes. El primero es que usando número en coma flotante el cero puede ser positivo o negativo. Esto es útil para representar ciertas soluciones matemáticas, pero en la mayoría de las situaciones no nos importa esa diferencia entre +0 y -0. La igualdad estricta los trata como un únicomvalor. El segundo caso tiene que ver con que los número en coma flotante incluyen el concepto NaN (Not a Number) como un posible valor para representar la solución a ciertos problemas matemáticos mal definidos, por ejemplo la adición de un infinito negativo a un infinito positivo. La igualdad estricta trata NaN como desigual con cualquier otro valore — incluyendo a sí mismo. (El único caso en el que x !== x es verdades en cuando x is NaN).

Igualdad débil usando ==

El operador igualdad débil compara la igualdad de dos valores después de convertir ambos valores a un tipo de datos común. Tras la conversión , la comparación que se lleva a cabo funciona exactamente como ===. La igual débil es una igualdad simétrica: A == B tiene una semática idéntica a B == A para cualquier valor que tengan A y B ( excepto por el orden de las .conversiones de tipo aplicadas)

Para los operandos de varios tipos funciona de la siguiente manera:

Operando B
Undefined Null Number String Boolean Object
Operando A Undefined true true false false false false
Null true true false false false false
Number false false A === B A === ToNumber(B) A === ToNumber(B) A == ToPrimitive(B)
String false false ToNumber(A) === B A === B ToNumber(A) === ToNumber(B) A == ToPrimitive(B)
Boolean false false ToNumber(A) === B ToNumber(A) === ToNumber(B) A === B ToNumber(A) == ToPrimitive(B)
Object false false ToPrimitive(A) == B ToPrimitive(A) == B ToPrimitive(A) == ToNumber(B) A === B

En la tabla previa, ToNumber(A) intenta convertir su argumento a número antes de realizar la compración. Su comportamiento es equivalente a +A (el operador unario +). ToPrimitive(A) intenta convertir su objeto argumento a un valor de tipo primitivo realizando varias secuencias de invocaciones A.toString y A.valueOf en A.

Tradicionalmente, y de acuerdo con la especificación ECMAScript, todos los objetos son débilmente desiguales comparándolos con undefined y null. Pero algunos nevegadores permiten que una cantidad muy limitada de clases de objetos (especifícamente , el objeto documento.all para todas las páginas), en algunos contextos, puedan actuar como si emularan el valor undefined. En ese contexto se evalúa como verdadero las igualdades débiles null == A y undefined == A, sí y sólo sí, A es un objecto que emula undefined. En cualquier otro caso la igual débil no será verdadera con undefined o null.

js
var num = 0;
var obj = new String("0");
var str = "0";
var b = false;

console.log(num == num); // true
console.log(obj == obj); // true
console.log(str == str); // true

console.log(num == obj); // true
console.log(num == str); // true
console.log(obj == str); // true
console.log(null == undefined); // true

// both false, except in rare cases
console.log(obj == null);
console.log(obj == undefined);

Algunos desarrolladores consideran que nunca es una buena idea usar este tipo de igualdad, la igualdad débil. El resultado cuando se usa la igualdad estricta es más fácil de predecir y , como no hay coerción de tipos durante la evaluación, es con casi total seguridad más rápida.

Igualdad Same-value

La igualdad Same-value se encarga de un último caso de uso: determinar si dos valores son funcionalmente idénticos en todos los contextos. (Este caso de uso es un caso de ejemplo del Liskov substitution principle.) Un ejemplo de esto ocurre cuando se intenta hacer mutable una propiedad inmutable.

js
// Add an immutable NEGATIVE_ZERO property to the Number constructor.
Object.defineProperty(Number, "NEGATIVE_ZERO", {
  value: -0,
  writable: false,
  configurable: false,
  enumerable: false,
});

function attemptMutation(v) {
  Object.defineProperty(Number, "NEGATIVE_ZERO", { value: v });
}

Object.defineProperty que lanzará una excepción cuando se intente cambiar una propiedad inmutable la cambiará, pero no hará nada si al solicitar el cambio actual . Si v es -0, no ha sido solicitado ningún cambio y no se lanzará ningún error. Pero si v es +0, Number . NEGATIVE_ZERO no tendrá más su valor inmutalbe. Internamente, al redefinir una propiedad inmutbale, el nuevo valor se compara con el valor actual usando la igualdad same-value.

El método Object.is nos proporciona la igualdad same-value.

Igualdad Same-value-zero

Similar a la igualdad same-value, pero +0 y -0 son considerados iguales.

Igualdad abstracta, igualdad estricta e igualdad same value en la especificación

En la especificación ES5, la comparación == (en-US) queda descrita en Section 11.9.3, The Abstract Equality Algorithm. La comparación === (en-US) en 11.9.6, The Strict Equality Algorithm. (Búscala y leela, son breves y fáciles de leer. Nota: lee el algoritmo de la igualdad estricta primero.) ES5 también describe, en Section 9.12, The SameValue Algorithm para uso interno del motor JS. Es, en su mayoría igual que el algoritmo de igualdad estricto, excepto porque 11.9..6.4 y 9.12.4 difieren en cómo tratar los Numbers. ES2015 simplemente propone exponer este algoritmo mediante el uso deObject.is.

Podemos ver con el igual doble y el triple que, con la excepción de hacer con antelación una comprobación de tipo en 11.9.6.1, el algorimto de igualdad estricta es un subconjunto del algorimot de igualdad abstracta porque 11.9.6.2-7 se corresponde con 11.9.3.1a-f.

¿ Un modelo para comprender las comparacions de igualdad?

Antes de ES2015, podíamos haber dicho sobre el igual doble y el igual triple que uno es una versión mejoradad del otro. Por ejemplo, alguien podría decir que el igual doble es una versión extendida del igual triple ya que el primero hace todo lo que hace el segundo pero añadiendo la conversión de operadores. Por ejemplo 6 == "6". (De manera alternativa alguien podría decir que el igual doble es la base y que el igual triple es una versión mejorada, ya que requiere que los dos operadores sean del mismo tipo y, por lo tanto, añade una restricción adicional. Qué afirmación es mejor para entender el modelo depende en tu punto de vista).

Sin embargo. esta manera de pensar sobre los operadores de igualdad proporcionados de manera nativa no es un modelo en el que podamos encuadrar la versión ES2015 de Object.isno es simplemente más débil que el doble igual o más estricto que el triple igual, ni tampoco ocupa un lugar intermedio (por ejemplo siendo a la vez más estricto que el igual doble y más débil que el igual triple). Podemos ver en la tabla inferior que esto se debe a la manera en la que Object.ismaneja NaN. Fíjate que si Object.is(Nan, Nan) evaluara a falso podríamos decir que que se encuadra dentro de la escala débil /estricta como algo más estricto que el triple igual, como un operador que distigue entre +0 y -0. Sin embargo en el manejo de NaNesto no es cierto. Simplemente debemos considerar Object.isen términos de sus características específicas y no en relación a su debilidad o rigidez dentro del especto de los operadores de igualdad.

x y == === Object.is
undefined undefined true true true
null null true true true
true true true true true
false false true true true
"foo" "foo" true true true
{ foo: "bar" } x true true true
0 0 true true true
+0 -0 true true false
0 false true false false
"" false true false false
"" 0 true false false
"0" 0 true false false
"17" 17 true false false
[1,2] "1,2" true false false
new String("foo") "foo" true false false
null undefined true false false
null false false false false
undefined false false false false
{ foo: "bar" } { foo: "bar" } false false false
new String("foo") new String("foo") false false false
0 null false false false
0 NaN false false false
"foo" NaN false false false
NaN NaN false false true

Cuando usar Object.is o el igual triple

Además de como trata NaN, generalmente, la única vez en la que Object.is posee un comportamiento especial hacia los ceros puede resultar de interés para usar ciertos esquemas de meta-programación, sobre todo en relación a los descriptores de porpiedades cuando es deseable que nuestro trabajo replique algunas de las características de Object.defineProperty. Si en tu situación no requiere de esto, lo mejor es evitar Object.isy usar ===. (en-US)Incluso si entre tus requisitos está poseer que la comparación entre dos valores NaNsea verdadera, generalmente es más fácil hacer un caso especial para ello (usando el método isNaNque está disponible en versiones previas de ECMAScript) que calcular cómo la operaciones van afectar a los posibles signos de los valores cero en tu comparación.

Aquí podemos ver una lista exhaustiva de los método y operadores nativos que pueden distinguir entre -0 y +0 en tu código:

- (unary negation) (en-US)

Obviamente negar 0 genera -0. Pero al abstracción de una expresión puede causar que un valor -0 se cuele sin darte cuenta Consideremos el siguiente ejemplo:

js
let stoppingForce = obj.mass * -obj.velocity;

Si obj.velocity is 0 (o se calcula como 0), se inserta -0 en ese lugar y este valor se propaga a stoppingForce.

Math.atan2

Empty

Math.ceil

Empty

Math.pow

Empty

Math.round

Se puede introducir un -0 dentro de una expresión como valor de retorno en estos método, incluso cuando -0 no sea uno de los parámetros. Por ejemplo usando Math.pow para elevar -Infinity a cualquier potencia negativa, los exponentes impares se evaluarán como -0. Consulta la documentación para más detalles sobre cada uno de los métodos.

Math.floor

Empty

Math.max

Empty

Math.min

Empty

Math.sin

Empty

Math.sqrt

Empty

Math.tan

Es posible obtener, en algunos casos, -0 como valor de retorno de estos métodos cuando -0 sea uno de los parámetros Por ejemplo Math.min(-0, +0) devuelve -0. Consulta la documentación para más detalles sobre cada uno de los métodos.

~ (en-US)

Empty

<< (en-US)

Empty

>> (en-US)

Cada uno de estos operadores usa el algoritmo ToInt32 de manera interna. Como sólo hay un representacion de 0 para el intero de 32-bit interno. -0 no sobrevivirá a la operación inversa. Por ejemplo Object.is(~~(-0), -0) y Object.is(-0 << 2 >> 2, -0) devolverán false.

Confiar en Object.is cuando no hay que tener en cuenta el signo de los ceros puede ser peligroso. Por supuesto para el caso contrario hará exactamente lo deseado.

Ver además