¿Cuál es la explicación de estos extraños comportamientos de JavaScript mencionados en la charla 'Wat' de CodeMash 2012?
La charla 'Wat' de CodeMash 2012 básicamente señala algunas peculiaridades extrañas de Ruby y JavaScript.
Hice un JSFiddle de los resultados en http://jsfiddle.net/fe479/9/ .
Los comportamientos específicos de JavaScript (ya que no conozco Ruby) se enumeran a continuación.
Encontré en JSFiddle que algunos de mis resultados no se correspondían con los del video y no estoy seguro de por qué. Sin embargo, tengo curiosidad por saber cómo se maneja JavaScript entre bastidores en cada caso.
Empty Array + Empty Array
[] + []
result:
<Empty String>
Tengo mucha curiosidad acerca del +
operador cuando se usa con matrices en JavaScript. Esto coincide con el resultado del vídeo.
Empty Array + Object
[] + {}
result:
[Object]
Esto coincide con el resultado del vídeo. ¿Que está pasando aqui? ¿Por qué es esto un objeto? ¿ Qué +
hace el operador?
Object + Empty Array
{} + []
result:
[Object]
Esto no coincide con el vídeo. El video sugiere que el resultado es 0, mientras que obtengo [Objeto].
Object + Object
{} + {}
result:
[Object][Object]
Esto tampoco coincide con el video, y ¿cómo genera una variable como resultado dos objetos? Quizás mi JSFiddle esté mal.
Array(16).join("wat" - 1)
result:
NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
Hacer wat + 1 da como resultado wat1wat1wat1wat1
...
Sospecho que este es un comportamiento sencillo: intentar restar un número de una cadena da como resultado NaN.
Aquí hay una lista de explicaciones de los resultados que está viendo (y se supone que debe ver). Las referencias que estoy usando son del estándar ECMA-262 .
[] + []
Cuando se utiliza el operador de suma, los operandos izquierdo y derecho se convierten primero en primitivas ( §11.6.1 ). Según §9.1 , convertir un objeto (en este caso una matriz) en una primitiva devuelve su valor predeterminado, que para objetos con un
toString()
método válido es el resultado de la llamadaobject.toString()
( §8.12.8 ). Para matrices, esto es lo mismo que llamararray.join()
( §15.4.4.2 ). Unir una matriz vacía da como resultado una cadena vacía, por lo que el paso 7 del operador de suma devuelve la concatenación de dos cadenas vacías, que es la cadena vacía.[] + {}
De manera similar a
[] + []
, ambos operandos se convierten primero en primitivos. Para "objetos Objeto" (§15.2), este es nuevamente el resultado de llamarobject.toString()
, que para objetos no nulos ni indefinidos es"[object Object]"
( §15.2.4.2 ).{} + []
El
{}
aquí no se analiza como un objeto, sino como un bloque vacío ( §12.1 , al menos siempre y cuando no fuerces esa declaración a ser una expresión, pero hablaremos de eso más adelante). El valor de retorno de los bloques vacíos está vacío, por lo que el resultado de esa declaración es el mismo que+[]
. El+
operador unario ( §11.4.6 ) regresaToNumber(ToPrimitive(operand))
. Como ya sabemos,ToPrimitive([])
es la cadena vacía y, según §9.3.1 ,ToNumber("")
es 0.{} + {}
Similar al caso anterior, el primero
{}
se analiza como un bloque con un valor de retorno vacío. De nuevo,+{}
es lo mismo queToNumber(ToPrimitive({}))
yToPrimitive({})
es"[object Object]"
(ver[] + {}
). Entonces, para obtener el resultado de+{}
, tenemos que aplicarloToNumber
en la cadena"[object Object]"
. Al seguir los pasos del §9.3.1 , obtenemosNaN
como resultado:Si la gramática no puede interpretar String como una expansión de StringNumericLiteral , entonces el resultado de ToNumber es NaN .
Array(16).join("wat" - 1)
Según §15.4.1.1 y §15.4.2.2 ,
Array(16)
crea una nueva matriz con longitud 16. Para obtener el valor del argumento a unir, los pasos 5 y 6 de §11.6.2 muestran que tenemos que convertir ambos operandos a un número usandoToNumber
.ToNumber(1)
es simplemente 1 ( §9.3 ), mientras queToNumber("wat")
nuevamente esNaN
según §9.3.1 . Siguiendo el paso 7 de §11.6.2 , §11.6.3 dicta queSi cualquiera de los operandos es NaN , el resultado es NaN .
Entonces el argumento
Array(16).join
esNaN
. Siguiendo §15.4.4.5 (Array.prototype.join
), tenemos que recurrirToString
al argumento, que es"NaN"
( §9.8.1 ):Si m es NaN , devuelve el String
"NaN"
.Siguiendo el paso 10 de §15.4.4.5 , obtenemos 15 repeticiones de la concatenación de
"NaN"
y la cadena vacía, lo que equivale al resultado que estás viendo. Cuando se usa"wat" + 1
en lugar de"wat" - 1
como argumento, el operador de suma se convierte1
en una cadena en lugar de convertirse"wat"
en un número, por lo que efectivamente llama aArray(16).join("wat1")
.
En cuanto a por qué estás viendo resultados diferentes para el {} + []
caso: cuando lo usas como argumento de función, estás forzando que la declaración sea ExpressionStatement , lo que hace imposible analizarlo {}
como un bloque vacío, por lo que en su lugar se analiza como un objeto vacío. literal.
Esto es más un comentario que una respuesta, pero por alguna razón no puedo comentar tu pregunta. Quería corregir tu código JSFiddle. Sin embargo, publiqué esto en Hacker News y alguien sugirió que lo volviera a publicar aquí.
El problema en el código JSFiddle es que ({})
(abrir llaves dentro de paréntesis) no es lo mismo que {}
(abrir llaves como comienzo de una línea de código). Entonces, cuando escribes, out({} + [])
estás obligando a {}
que sea algo que no es cuando escribes {} + []
. Esto es parte de la funcionalidad general de Javascript.
La idea básica era que JavaScript simple quería permitir ambas formas:
if (u)
v;
if (x) {
y;
z;
}
Para ello se hicieron dos interpretaciones de la llave de apertura: 1. no es obligatoria y 2. puede aparecer en cualquier lugar .
Este fue un movimiento en falso. El código real no tiene una llave de apertura que aparezca en medio de la nada, y el código real también tiende a ser más frágil cuando usa la primera forma en lugar de la segunda. (Aproximadamente una vez cada dos meses en mi último trabajo, me llamaban al escritorio de un compañero de trabajo cuando sus modificaciones a mi código no funcionaban, y el problema era que habían agregado una línea al "si" sin agregar caracteres rizados llaves. Eventualmente adopté el hábito de que las llaves siempre son necesarias, incluso cuando solo estás escribiendo una línea).
Afortunadamente, en muchos casos eval() replicará toda la funcionalidad de JavaScript. El código JSFiddle debería leer:
function out(code) {
function format(x) {
return typeof x === "string" ?
JSON.stringify(x) : x;
}
document.writeln('>>> ' + code);
document.writeln(format(eval(code)));
}
document.writeln("<pre>");
out('[] + []');
out('[] + {}');
out('{} + []');
out('{} + {}');
out('Array(16).join("wat" + 1)');
out('Array(16).join("wat - 1")');
out('Array(16).join("wat" - 1) + " Batman!"');
document.writeln("</pre>");
[Además, es la primera vez que escribo document.writeln en muchos, muchos años, y me siento un poco sucio al escribir cualquier cosa que involucre tanto document.writeln() como eval().]
Apoyo la solución de @ Ventero. Si lo desea, puede entrar en más detalles sobre cómo +
convierte sus operandos.
Primer paso (§9.1): convierta ambos operandos en primitivos (los valores primitivos son undefined
booleanos null
, números, cadenas; todos los demás valores son objetos, incluidas matrices y funciones). Si un operando ya es primitivo, ya está. Si no, es un objeto obj
y se realizan los siguientes pasos:
- Llamar
obj.valueOf()
. Si devuelve una primitiva, ya está. Las instancias directas deObject
y las matrices regresan solas, por lo que aún no ha terminado. - Llamar
obj.toString()
. Si devuelve una primitiva, ya está.{}
y[]
ambos devuelven una cadena, así que ya está. - De lo contrario, lanza un
TypeError
.
Para las fechas, se intercambian los pasos 1 y 2. Puede observar el comportamiento de conversión de la siguiente manera:
var obj = {
valueOf: function () {
console.log("valueOf");
return {}; // not a primitive
},
toString: function () {
console.log("toString");
return {}; // not a primitive
}
}
Interacción ( Number()
primero se convierte a primitivo y luego a número):
> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value
Segundo paso (§11.6.1): Si uno de los operandos es una cadena, el otro operando también se convierte en cadena y el resultado se produce al concatenar dos cadenas. De lo contrario, ambos operandos se convierten a números y el resultado se produce sumándolos.
Explicación más detallada del proceso de conversión: “¿ Qué es {} + {} en JavaScript? "
Podemos referirnos a la especificación y eso es excelente y muy preciso, pero la mayoría de los casos también se pueden explicar de una manera más comprensible con las siguientes afirmaciones:
+
y-
los operadores trabajan sólo con valores primitivos. Más específicamente,+
(suma) funciona con cadenas o números, y+
(unario) y-
(resta y unario) funcionan solo con números.- Todas las funciones u operadores nativos que esperan un valor primitivo como argumento, primero convertirán ese argumento al tipo primitivo deseado. Se hace con
valueOf
otoString
, que están disponibles en cualquier objeto. Esa es la razón por la cual dichas funciones u operadores no arrojan errores cuando se invocan en objetos.
Entonces podemos decir que:
[] + []
es igual queString([]) + String([])
cual es igual que'' + ''
. Mencioné anteriormente que+
(suma) también es válida para números, pero no existe una representación numérica válida de una matriz en JavaScript, por lo que en su lugar se utiliza la suma de cadenas.[] + {}
es igual queString([]) + String({})
cual es igual que'' + '[object Object]'
{} + []
. Este merece más explicación (ver respuesta de Ventero). En ese caso, las llaves no se tratan como un objeto sino como un bloque vacío, por lo que resulta ser lo mismo que+[]
. Unario+
sólo funciona con números, por lo que la implementación intenta obtener un número de[]
. Primero intentavalueOf
que en el caso de matrices devuelva el mismo objeto, luego intenta el último recurso: la conversión de untoString
resultado a un número. Podemos escribirlo como+Number(String([]))
cuál es igual que+Number('')
cuál es igual que+0
.Array(16).join("wat" - 1)
la resta-
solo funciona con números, por lo que es lo mismo que:Array(16).join(Number("wat") - 1)
, ya que"wat"
no se puede convertir en un número válido. RecibimosNaN
, y cualquier operación aritmética sobreNaN
los resultados conNaN
, por lo que tenemos:Array(16).join(NaN)
.
Para reforzar lo que se ha compartido anteriormente.
La causa subyacente de este comportamiento se debe en parte a la naturaleza débilmente tipada de JavaScript. Por ejemplo, la expresión 1 + “2” es ambigua ya que existen dos posibles interpretaciones basadas en los tipos de operandos (int, string) y (int int):
- El usuario intenta concatenar dos cadenas, resultado: "12"
- El usuario intenta sumar dos números, resultado: 3
Así, con distintos tipos de insumos, las posibilidades de producción aumentan.
El algoritmo de suma
- Forzar operandos a valores primitivos.
Las primitivas de JavaScript son cadena, número, nulo, indefinido y booleano (el símbolo estará disponible próximamente en ES6). Cualquier otro valor es un objeto (por ejemplo, matrices, funciones y objetos). El proceso de coerción para convertir objetos en valores primitivos se describe así:
Si se devuelve un valor primitivo cuando se invoca object.valueOf(), devuelva este valor; de lo contrario, continúe
Si se devuelve un valor primitivo cuando se invoca object.toString(), entonces se devuelve este valor; de lo contrario, continúa
Lanzar un error de tipo
Nota: Para valores de fecha, el orden es invocar toString antes de valueOf.
Si algún valor de operando es una cadena, entonces haga una concatenación de cadenas
De lo contrario, convierta ambos operandos a su valor numérico y luego sume estos valores
Conocer los distintos valores de coerción de los tipos en JavaScript ayuda a aclarar los resultados confusos. Vea la tabla de coerción a continuación.
+-----------------+-------------------+---------------+
| Primitive Value | String value | Numeric value |
+-----------------+-------------------+---------------+
| null | “null” | 0 |
| undefined | “undefined” | NaN |
| true | “true” | 1 |
| false | “false” | 0 |
| 123 | “123” | 123 |
| [] | “” | 0 |
| {} | “[object Object]” | NaN |
+-----------------+-------------------+---------------+
También es bueno saber que el operador + de JavaScript es asociativo por la izquierda, ya que esto determina cuál será el resultado en los casos que involucren más de una operación +.
Aprovechar Así 1 + "2" dará "12" porque cualquier adición que involucre una cadena siempre usará de forma predeterminada la concatenación de cadenas.
Puedes leer más ejemplos en esta publicación de blog (descargo de responsabilidad, lo escribí yo).