Operadores de igualdad en Javascript

24 Ene 2011

Hay ocasiones en las que los operadores de igualdad en Javascript pueden causar más de un dolor de cabeza. Entender un poco mejor el funcionamiento de los mismos, puede evitarnos errores difíciles de detectar durante el flujo de un programa.

Echemos un vistazo detenido a las particularidades de los operadores de igualdad tan característicos de un lenguaje de tipado blando como es Javascript.

Igualdades en Javascript

Tal y como nos explica Crockford en el apéndice B de su Good Parts, Javascript posée dos grupos de operadores de igualdad: === y !==, y sus malvados hermanos gemelos == y !=.

El primer grupo, llamado estricto, actúa como cabe esperar: si dos operadores son del mismo tipo y tienen el mismo valor, === devolverá true y !== devolverá false.

El grupo formado por los hermanos gemelos, llamado no-estricto, tratará de realizar la comparación incluso si los tipos de datos de ambos términos son diferentes. Para ello, realizará las conversiones necesarias pudiendo provocar durante este proceso resultados inesperados. Dado que este grupo corresponde con el menos intuitivo, será el primero que analicemos.

Igualdad no estricta ‘==’

La igualdad no-estricta se encarga de comparar los dos términos sin requerir que ambos pertenezcan al mismo tipo de datos. En caso de no corresponderse, el intérprete Javascript realiza una conversión interna de los mismos para poder realizar la comparación. Si los términos son coincidentes, el resultado es true mientras que en caso contrario, se obtiene false.

Para ilustrar este comportamiento, debemos recordar que, según la especificación ECMAScript, la igualdad en este lenguaje es no-transitiva.

Esto siginifica que, si tenemos:

a == b;
b == c;

No tiene necesariamente que cumplirse

a == c;

Un ejemplo de esto sería el siguiente código:

var a = new String( 'foo' );
var b = "foo";
var c = new String( 'foo' );
 
console.log( a == b ); // true
console.log( b == c ); // true
console.log( a == c ); // false

Estos valores son el resultados de realizar la conversión (coerción) de los distintos tipos de datos antes de poder realizar la comparación.
Estas conversiones, se realizan siguiendo un orden determinado. Para el caso de las igualdades no-estrictas, la conversión principal es siempre ToNumber: siempre que los tipos de datos sean diferentes, Javascript aplicará en primer lugar una conversión ToNumber a cada uno de los operandos:

console.log( 1 == "1" ); // true -> ToNumber( "1" ) -> 1 == 1

Sin embargo, para el caso de los objetos, la primera conversión aplicada es ToPrimitive. Si tras la misma, aún no puede realizarse la comparación, se prosigue con ToNumber.

console.log( 'foo' == {} ); // false
console.log( 'foo' == ['foo'] ); // true -> valueOf( [ 'foo' ] ) -> 'foo' == 'foo'

En el caso de los valores booleanos, no tenemos una conversión ToBoolean de los términos, pero si que se aplica la mencionada ToNumber. Esto quiere decir que para un argumento booleano, la conversión ToNumber resulta 1 si el valor es verdadero (true) y 0 si es falso (false):

console.log( 1 == true ); // true -> ToNumber( true ) -> 1 == 1
console.log( 0 == true ); // false -> ToNumber( true ) -> 1 != 0

Hay que prestar una atención especial a los valores denominados ‘falsy‘ en estas igualdades no-estrictas:

console.log( 0 == 0 ); // true
console.log( "" == 0 ); // true
console.log( false == 0 ); // true
console.log( NaN == 0 ); // false
console.log( null == 0 ); // false
console.log( undefined == 0 ); // false

NOTA: El 0 del segundo término, podemos sustituirlo en todos los casos por ‘false’ obteniendo el mismo resultado.

Las tres discrepancias las encontramos con NaN, null y undefined.
El valor NaN supone un cuantificador especial definido en el IEEE 754-2008. Se reserva para para valores que «no son un número» (Not A Number). Sin embargo, Javascript considera a NaN como un número más:

console.log( typeof NaN ); // number

Este valor se obtiene cuando tratamos de convertir una cadena en un número y esta no tiene el formato apropiado para la conversión:

console.log( + '0' ); // 0
console.log( + 'foo' ); // NaN

NOTA: El operador unario «+», convierte una cadena dada en un número.

Otra particularidad de NaN es que no es igual a sí mismo:

console.log( NaN == NaN ); // false
console.log( NaN === NaN ); // false

En cuanto a los valores undefined y null tampoco no son iguales a false en la comparación no-estricta. Esto se debe de nuevo a la conversión ToNumber aplicada internamente en lugar de a una esperada ToBoolean (paso 19 del apartado 11.9.3 de la especificación).

console.log( null == false ); // false -> null != ToNumber(false)
console.log( undefined == false ); // false -> undefined != ToNumber(false)

undefined y NaN no son constantes sino son variables globales, es por ello que las comparaciones donde estos valores actúen como términos, son siempre problemáticos en el modo no-estricto.

Comparación estricta ‘===’

En Javascript, la igualdad estricta exige que los términos a comparar correspondan al mismo tipo de datos y sus valores sean iguales. En caso de darse estas condiciones, el valor obtenido es true, mientras que para lo contrario, obtendremos false.

A diferencia de la comparación no-estricta, no se realiza ningún tipo de conversión en los términos.

console.log( '1' === 1 ); // false

Para entender el anterior resultado, sólo necesitamos comprobar el tipo de datos a comparar:

console.log( typeof '1' ); // string
console.log( typeof 1 ); // number

Una cuestión importante es entender la diferencia de los tipos de datos según el modo de declarar una variable. Si utilizamos la notación literal, el tipo de datos será una primitiva, mientras que si utilizamos un constructor (comando new), el tipo de datos será un objeto:

var a = new Number(1);
var b = 1;
console.log( a == b ); // true
console.log( a === b ); // false
 
var a = new String('foo');
var b = 'foo';
console.log( a == b ); // true
console.log( a === b ); // false

En este caso, el operador == comprueba el valor de los dos objetos y devuelve true. Sin embargo, === comprueba que no se tratan del mismo tipo de objeto devolviendo false.

Curiosidades sobre comparaciones y coerción de datos:

– Una forma de determinar si un número es un entero en Javascript mediante comparación estricta:

x = 1;
if ( x === Math.floor(x) ){
  // x is an integer
};

Si quisiéramos añadir este método al prototipo Object, deberemos tener cuidado de mantener la correspondencia de tipos:

Object.prototype.isInteger = function() {
  return +this === Math.floor(this);
}
 
var foo = '123';
var bar = 123;
var other = 'asd';
 
console.log( foo.isInteger() ); // true
console.log( bar.isInteger() ); // true
console.log( other.isInteger() ); // false

– El ‘valor’ de true en operaciones matemáticas :

console.log( ( true + 1 ) === 2 ); // true
console.log( ( true + true ) === 2 ); // true
console.log( true === 2 ); // false
console.log( true === 1 ); // false

Para descubrir más casos interesantes fruto de la coerción en Javascript, o de las propias características de su tipado, el blog wtfjs contiene una fantástica colección de ellos.

Todos estos casos vienen a subrayar los posibles problemas que pueden derivarse del uso incorrecto de los dos tipos de comprobación de igualdad que ofrece Javascript. La siguiente pregunta es casi obligada:

¿ Cuándo usar ‘==’ y cuando ‘===’ ?

De un modo general, Douglas Crockford aconseja no utilizar nunca la comparación no-estricta. Para él, las reglas que determinan el resultado en este tipo de comparaciones son complejas de recordar, poco intuitivas y dan lugar a equívocos.
Si no obstante decidimos usar ambas, tendremos que preguntarnos sobre la naturaleza de los términos que estamos comparando: para los casos en los que no estamos seguros sobre qué tipo de datos devolverá una función o qué valores se van a comparar, resulta mucho más seguro recurrir a la comparación estricta ‘===’. Para el resto de los casos, la comparación no-estricta ‘==’ puede resultar suficiente.
Librerías como jQuery incluyen constructores que combinan typeof con comparaciones estrictas que permiten al desarrollador realizar comparaciones seguras con ‘==’. Sin embargo, también podemos encontrarnos con otras librerías que no lo permiten (un ejemplo de esto podría ser ExtJS).
En cuanto a temas de rendimiento, si comparamos términos del mismo tipo, la igualdad no-estricta ‘==’ resulta más rápida. Para tipos diferentes, ‘===’ resulta lógicamente más rápido al no precisar de una conversión previa a la comparación.

Conclusión

Como la mayoría de las veces, conocer el funcionamiento de un sistema es más productivo que limitarnos a erradicar sus comandos más conflictivos. En el caso de los operadores de igualdad en Javascript, entender el algoritmo utilizado por el intérprete puede permitirnos seleccionar entre uno método u otro de forma segura o localizar rápidamente posibles errores en la estructura de un código.

Mas información

Dmitry A. Soshnikov, ECMAScript Equality Operators.

ECMAScript Language Specification, Non Official Version.

DevGuru, Comparison Operators in Javascript.

Más:

{2} Comentarios.

  1. Alejandro

    Muy interesante, todos los días se aprende algo, no conocía la existencia de los operadores estrictos === !==

    Buen post!

  2. yordito

    Buen post, gracias!!!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *