Este post es un homenaje a la fantástica charla «Wat» de Gary Bernhardt en la que señala las peculiaridades de algunas construcciones de lenguaje en Ruby y JavaScript. Si aún no ha visto la charla, le recomiendo encarecidamente que se tome el tiempo y haga precisamente eso. Solo dura unos 4 minutos y es muy entretenido, lo prometo.
En su charla, Gary muestra estos cuatro fragmentos de código JavaScript:
Vemos muchos corchetes, llaves y signos de más. Esto es lo que estos fragmentos de evaluar a:
+ == ""
+ {} == ""
{} + == 0
{} + {} == NaN
Cuando vi estos ejemplos, por primera vez, pensé: «Wow, que se ve desordenado!»Los resultados pueden parecer inconsistentes o incluso arbitrarios,pero tengan paciencia conmigo. ¡Todos estos ejemplos son en realidad muy consistentes y no tan malos como parecen!
#Fragmento #1: +
Empecemos con el primer fragmento:
+ // ""
Como podemos ver, aplicar el operador +
a dos matrices vacías resulta en una cadena vacía. Esto se debe a que la representación de cadena de un array es la representación de cadena de todos sus elementos, concatenada junto con comas:
.toString()// "1,2,3".toString()// "1,2".toString()// "1".toString()// ""
Una matriz vacía no contiene ningún elemento, por lo que su representación de cadena es una cadena vacía. Por lo tanto, la concatenación de dos cadenas vacías es solo otra cadena vacía.
#Fragmento #2: + {}
Hasta ahora, todo bien. Examinemos ahora el segundo fragmento:
+ {}// ""
Tenga en cuenta que, como no estamos tratando con dos números, el operador +
realiza una vez más la concatenación de cadenas en lugar de la adición de dos valores numéricos.
En la sección anterior, ya hemos visto que la representación de cadena de un array vacío es una cadena vacía. La representación de cadena del literal del objeto vacío aquí es el valor predeterminado ""
. Anteponer una cadena vacía no cambia el valor, por lo que ""
es el resultado final.
En JavaScript, los objetos pueden implementar un método especial llamado toString()
que devuelve una representación de cadena personalizada del objeto al que se llama el método. Nuestro literal de objeto vacío no implementa tal método, por lo que volvemos a la implementación predeterminada del prototipo Object
.
#Fragmento #3: {} +
Yo diría que hasta ahora, los resultados no han sido demasiado inesperados. Simplemente han estado siguiendo las reglas de coerción de tipos y representaciones de cadenas predeterminadas en JavaScript.
Sin embargo, {} +
es donde los desarrolladores comienzan a confundirse:
{} + // 0
¿Por qué vemos 0
(el número cero) si escribimos la línea anterior en una REPL de JavaScript como la consola del navegador? ¿No debería ser el resultado una cadena, como lo fue + {}
?
Antes de resolver el acertijo, considere las tres formas diferentes en que se puede usar el operador +
:
// 1) Addition of two numeric values2 + 2 == 4// 2) String concatenation of two values"2" + "2" == "22"// 3) Conversion of a value to a number+2 == 2+"2" == 2
En los dos primeros casos, el operador +
es un operador binario porque tiene dos operandos (a la izquierda y a la derecha). En el tercer caso, el operador +
es un operador unario porque solo tiene un operando único (a la derecha).
También considere los dos posibles significados de {}
en JavaScript. Por lo general, escribimos {}
para significar un literal de objeto vacío, pero si estamos en posición de declaración, la gramática de JavaScript especifica {}
para significar un bloque vacío. El siguiente fragmento de código define dos bloques vacíos, ninguno de los cuales es un literal de objeto:
{}// Empty block{ // Empty block}
Echemos un vistazo a nuestro fragmento de nuevo:
{} +
Permítanme cambiar un poco el espacio en blanco para que quede más claro cómo ve el código el motor JavaScript:
{ // Empty block}+;
Ahora podemos ver claramente lo que está pasando aquí. Tenemos una instrucción de bloque seguida de otra instrucción que contiene una expresión unaria +
que opera en una matriz vacía. El punto y coma final se inserta automáticamente de acuerdo con las reglas de ASI (inserción automática de punto y coma).
Puede verificar fácilmente en la consola de su navegador que +
se evalúa como 0
. La matriz vacía tiene una cadena vacía como representación de cadena, que a su vez es convertida al número cero por el operador +
. Por último, la consola del navegador informa del valor de la última instrucción (+
, en este caso).
Alternativamente, puede alimentar ambos fragmentos de código a un analizador de JavaScript como Esprima y comparar los árboles de sintaxis abstractas resultantes. Aquí está el AST para + {}
:
{ "type": "Program", "body": }, "right": { "type": "ObjectExpression", "properties": } } } ], "sourceType": "script"}
Y aquí está el AST para {} +
:
{ "type": "Program", "body": }, { "type": "ExpressionStatement", "expression": { "type": "UnaryExpression", "operator": "+", "argument": { "type": "ArrayExpression", "elements": }, "prefix": true } } ], "sourceType": "script"}
La confusión se debe a un matiz de la gramática de JavaScript que utiliza llaves tanto para literales de objetos como para bloques. En la posición de instrucción, una llave de apertura inicia un bloque, mientras que en la posición de expresión una llave de apertura inicia un literal de objeto.
#Fragmento #4: {} + {}
Por último, echemos un vistazo rápidamente a nuestro último fragmento {} + {}
:
{} + {}// NaN
Bueno, agregar dos literales de objetos es literalmente «no un número», pero ¿estamos agregando dos literales de objetos aquí? ¡No dejes que los aparatos te engañen de nuevo! Esto es lo que está pasando:
{ // Empty block}+{};
Es más o menos el mismo trato que en el ejemplo anterior. Sin embargo, ahora estamos aplicando el operador unario plus a un literal de objeto vacío. Eso es básicamente lo mismo que hacer Number({})
, lo que resulta en NaN
porque nuestro objeto literal no se puede convertir en un número.
Si desea que el motor JavaScript analice el código como dos literales de objeto vacíos, envuelva el primero (o todo el código) entre paréntesis. Ahora debería ver el resultado esperado:
({}) + {}// ""({} + {})// ""
El paréntesis de apertura hace que el analizador sintáctico intente reconocer una expresión, por lo que no trata a {}
como un bloque (que sería una instrucción).
# Resumen
Ahora debería ver por qué los cuatro fragmentos de código evalúan la forma en que lo hacen. No es arbitrario o aleatorio en absoluto; las reglas de coerción de tipo se aplican exactamente como se establece en la especificación y la gramática del lenguaje.
Solo tenga en cuenta que si una llave de apertura es el primer carácter que aparece en una instrucción, se interpretará como el inicio de un bloque en lugar de un literal de objeto.