JavaScript es un lenguaje que nos da una libertad para la que no estamos preparados.

Todo se puede sumar y restar. Todo es verdadero o falso. Las cosas son iguales o diferentes entre sí, de acuerdo a cómo las comparemos. De hecho, hay valores que son diferentes a sí mismos.

Hasta podemos modificar los prototype para que true.valueOf() evalúe a false.

Los problemas (o gotchas) más comunes son ocasionados por uno de los siguientes motivos:

Execution context y hoisting

¿Qué mensaje se mostrará después de llamar al segundo alert?

function hoisting() {
  var mensaje = "Uno!";

  function saludo() {
    mensaje = "Dos!";
  }

  function otroSaludo(condition) {
    mensaje = "Tres!";
    if (condition > 1) {
      var saludo, mensaje;
      mensaje = "Cuatro!";
      saludo = "Cinco!";
    } else {
      mensaje = "Seis!";
    }
    return
  }

  saludo();
  alert(mensaje);
  otroSaludo(0);
  alert(mensaje);
}

La respuesta correcta es “Dos!”, antiintuitivamente.

Nótese que el código dentro del método otroSaludo es confuso intencionalmente , para distraer de la declaración de la variable, como podría suceder en un escenario real.

Esto sucede porque el valor “Tres!” se asigna a la variable local, a pesar de aparentemente producirse antes de ser redefinida. Lo mismo sucede con el valor “Seis!”, que es asignada en el bloque else, fuera del bloque en que se redefine mensaje.

El contexto de una variable JavaScript definida con la palabra var es siempre el de la función que la rodea. Sin importar en qué línea esté definida.

Este proceso se llama [Hoisting].
Lo que sucede es que las declaraciones de funciones y variables (usando var) se cargan en memoria durante la fase de compilación.

Nota: declarar las variables con let o const causa el comportamiento más intuitivo. En el ejemplo anterior, el segundo alert mostraría el mensaje “Seis!”. Es por este motivo que siempre se recomienda su uso en lugar del anticuado var.

Event loop y asincronicidad

Otro comportamiento similar se puede observar en el siguiente ejemplo:

 for (var i = 0; i < 100; i++) {
  setTimeout(function callback() {
   if (i === 100) {
    console.log("10 x 10 son: " + i);
   }
  }, 150)// milisegundos 
}

setTimeout empuja la función callback a la [cola de mensajes] después de 150ms, luego la función será movida a la [pila de ejecución] cuando ésta se vacíe (en este caso, cuando el for termine sus 100 iteraciones).
En JavaScript, toda función es ejecutada hasta que termine, sin interrupciones, por lo que i alcanzará el valor 100 antes de que se ejecute el primer console.log.
Esto causará que obtengamos 100 veces el mensaje: "10 x 10 son: 100", en lugar de solo obtenerlo una vez.

Nota: Nuevamente, declarando i como let i = 0; podemos obtener el comportamiento más intuitivo,
dado que la variable será local a cada iteración

setTimeout no garantiza que el callback será ejecutado en exactamente la cantidad de milisegundos provistos como parámetro, sino que pasarán al menos tantos milisegundos hasta su ejecución.
Veamos un ejemplo:

console.time(str) inicia un temporizador con un nombre str, console.timeEnd(str)
lo finaliza y muestra la duración en milisegundos.

Al comparar ambas ejecuciones podemos ver que, a pesar de setear nuestro timeout en 0 milisegundos, el segundo ejemplo es (prácticamente) igual al primero. Esto es porque nuestro bucle se ejecutó completamente antes de vaciar la pila y permitir que se ejecute console.timeEnd("time!")

Comparaciones y tipos

Antes di a entender que la igualdad en JavaScript es un tema delicado. Uno espera que, más allá del tipo, cada instancia de lo que sea sea igual a sí misma. Y que un número incrementado en un valor n sea diferente a un número sin dicho incremento. Entonces, ¿qué está sucediendo en el fragmento de código que acabamos de ver?

NaN (“not a number”) es un número, y es distinto a todos los números, incluyendo a NaN (como en la mayoría de los lenguajes de programación).  Asimismo, Infinity y -Infinity también son números y como tales pueden ser sumados, restados, multiplicados, etc. Toda operación matemática que incluya a ±Infinity dará como resultado ±Infinity (excepto ±Infinity/±Infinity, o la resta de dos infinitos). Una vez entendido esto, resulta fácil ver que en el ejemplo anterior, a es igual a NaN y c es igual a Infinity.

Para comprobar si un valor es NaN podemos usar el método isNaN(valor). Para ±Infinity podemos usar isFinite(valor), pero debemos tener en cuenta que isFinite retorna false para cualquier expresión que evalúe a ±Infinity o NaN después de ser convertido a número. Por lo que isFinite(null) retorna true, mientras que isFinite("null") retorna false. Otras formas de comprobar por ±Infinity incluyen [comparando el valor por sí mismo dividido 0, o incrementado en 1].

Si hablamos de comparaciones, no podemos evitar hablar de los truthy y falsy values. Los falsy (valores que evalúan a false) son:

  • 0
  • false
  • null
  • NaN
  • undefined
  • ""

Todos los demás valores son truthy (evalúan a true).

La comparación no estricta (doble igual: ==) trata como equivalentes a 0, false y "". En cambio null y undefined solo son iguales a sí mismos y entre sí . NaN es distinto a todo (como mencionamos antes). La comparación estricta (triple igual===) sí los trata como diferentes (false !== 0, null !== undefined, etc).

Y justo cuando creíamos que las excepciones se terminaban, llegan las excepciones de las excepciones…

0 == "0", a pesar de que "0" es truthy, [] == false, a pesar de que [] es truthy, lo que significa que tanto ![] == false como [] == false evalúan a true.

Ah! y [1] == 1. Y "2" == [2], o 3 == "3".

En general, se puede pensar en el doble igual como un operador que hace su mejor esfuerzo para igualar dos valores. Esto incluye convertir números en string, booleanos en número, objetos en string, entre otras cosas. (sí, NaN != NaN , a pesar de todo)

Otra característica del doble igual es que no es transitivo. Por ejemplo: [0.] == "0" && 0. == "0." && [0.] != "0." .

Comentarios finales

El objetivo de este artículo es el de prevenir que un desarrollador distraido caiga en una de las muchas trampas de JavaScript. Que pueda ver un if(a == true) e inmediatamente sienta un escalofrío que lo impulse a cambiarlo por algo más robusto. Que pueda identificar un setTimeout defectuoso, o que pueda aprovechar este conocimiento para insertar un setTimeout de provecho.

Hay temas que quedan fuera, el más importante (sin dudas) es el de this, la palabra mágica de JavaScript. Pero también hay que hablar de performance, de paradigma funcional, promesas, callbacks…