¿Cuál es la explicación de estos extraños comportamientos de JavaScript mencionados en la charla 'Wat' de CodeMash 2012?

Resuelto NibblyPig asked hace 12 años • 5 respuestas

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.

NibblyPig avatar Jan 27 '12 18:01 NibblyPig
Aceptado

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 .

  1. [] + []

    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 llamada object.toString()( §8.12.8 ). Para matrices, esto es lo mismo que llamar array.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.

  2. [] + {}

    De manera similar a [] + [], ambos operandos se convierten primero en primitivos. Para "objetos Objeto" (§15.2), este es nuevamente el resultado de llamar object.toString(), que para objetos no nulos ni indefinidos es "[object Object]"( §15.2.4.2 ).

  3. {} + []

    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 ) regresa ToNumber(ToPrimitive(operand)). Como ya sabemos, ToPrimitive([])es la cadena vacía y, según §9.3.1 , ToNumber("")es 0.

  4. {} + {}

    Similar al caso anterior, el primero {}se analiza como un bloque con un valor de retorno vacío. De nuevo, +{}es lo mismo que ToNumber(ToPrimitive({}))y ToPrimitive({})es "[object Object]"(ver [] + {}). Entonces, para obtener el resultado de +{}, tenemos que aplicarlo ToNumberen la cadena "[object Object]". Al seguir los pasos del §9.3.1 , obtenemos NaNcomo resultado:

    Si la gramática no puede interpretar String como una expansión de StringNumericLiteral , entonces el resultado de ToNumber es NaN .

  5. 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 usando ToNumber. ToNumber(1)es simplemente 1 ( §9.3 ), mientras que ToNumber("wat")nuevamente es NaNsegún §9.3.1 . Siguiendo el paso 7 de §11.6.2 , §11.6.3 dicta que

    Si cualquiera de los operandos es NaN , el resultado es NaN .

    Entonces el argumento Array(16).joines NaN. Siguiendo §15.4.4.5 ( Array.prototype.join), tenemos que recurrir ToStringal 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" + 1en lugar de "wat" - 1como argumento, el operador de suma se convierte 1en una cadena en lugar de convertirse "wat"en un número, por lo que efectivamente llama a Array(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.

Ventero avatar Jan 27 '2012 12:01 Ventero

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('&gt;&gt;&gt; ' + 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().]

CR Drost avatar Jan 30 '2012 18:01 CR Drost

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 undefinedbooleanos 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 objy se realizan los siguientes pasos:

  1. Llamar obj.valueOf(). Si devuelve una primitiva, ya está. Las instancias directas de Objecty las matrices regresan solas, por lo que aún no ha terminado.
  2. Llamar obj.toString(). Si devuelve una primitiva, ya está. {}y []ambos devuelven una cadena, así que ya está.
  3. 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? "

Axel Rauschmayer avatar Jan 30 '2012 04:01 Axel Rauschmayer

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 valueOfo toString, 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 que String([]) + 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 que String([]) + 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 intenta valueOfque en el caso de matrices devuelva el mismo objeto, luego intenta el último recurso: la conversión de un toStringresultado 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. Recibimos NaN, y cualquier operación aritmética sobre NaNlos resultados con NaN, por lo que tenemos: Array(16).join(NaN).
Mariusz Nowak avatar Jan 30 '2012 11:01 Mariusz Nowak

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

  1. 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.

  1. Si algún valor de operando es una cadena, entonces haga una concatenación de cadenas

  2. 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).

AbdulFattah Popoola avatar Oct 04 '2015 16:10 AbdulFattah Popoola