Hay cuatro algoritmos de igualdad en ES2015:
- Comparación de Igualdad Abstracta (
==
) - Comparación de Igualdad Estricta (
===
): usado por byArray.prototype.indexOf
,Array.prototype.lastIndexOf
, ycase
-matching - SameValueZero: Usado por
lso constructores de %TypedArray%
yArrayBuffer
, así como por las operaciones deMap
ySet
, y los nuevosString.prototype.includes
ide ES016 - SameValue: usado en el resto de los casos
JavaScript proporciona tres operaciones distintas para comparar la igualdad de dos elementos:
- Igualdad estricta (o "triple igual" o "identida") using ===,
- igualdad débil o relajada ("doble igual") usando ==,
- y
Object.is
(nuevo ECMAScript 2015).
Elegir una u otra operación depende del tipo de comparación que 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 último 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 cómo se manejan los los tipos primitivos; ninguna de ellos compara si los parámetros son similares en relación a su estructura. Para comparar dos objectos primitivos , x ey, 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 un tipo de diferente 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 son del mismo valor, o si uno es +0 y otro -0.
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 operando 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
.
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 toda seguridad más rápida.
Igualdad Same-value
La igualdad Same-value se encarga deun último caso de uso: determinar si dos valores son funcionalmente idénticos en todos los contextos. (Este caso de uso es una caso de ejemplo del Liskove substitution principle.) Un ejemplo de esto ocurre cuando se intenta hacer mutable una propiedad inmutable.
// 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
==
queda descrita en
Section 11.9.3, The Abstract Equality Algorithm. La comparación ===
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 de
Object.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.is. Object.is
no 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.is
maneja 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 NaN
esto no es cierto. Simplemente debemos considerar Object.is
en 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 carqacterísticas de Object.defineProperty
. Si en tu situación no requiere de esto, lo mejor es evitar Object.is
y usar ===.
Incluso si entre tus requisitos está poseer que la comparación entre dos valores NaN
sea verdadera, generalmente es más fácil hacer un caso especial para ello (usando el método isNaN
que 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:
-
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:let stoppingForce = obj.mass * -obj.velocity
Si o
bj.velocity
is0
(o se calcula como0
), se inserta-0
en ese lugar y este valor se propaga astoppingForce
.
- 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.
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.
~
<<
>>
- 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.