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

753

La charla 'Wat' para CodeMash 2012 básicamente señala algunas peculiaridades extrañas con Ruby y JavaScript.

He hecho un JSFiddle de los resultados en http://jsfiddle.net/fe479/9/ .

Los comportamientos específicos de JavaScript (como no sé Ruby) se enumeran a continuación.

En el JSFiddle descubrí 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 trabajando detrás de escena en cada caso.

Empty Array + Empty Array
[] + []
result:
<Empty String>

Tengo mucha curiosidad sobre el +operador cuando se usa con matrices en JavaScript. Esto coincide con el resultado del video.

Empty Array + Object
[] + {}
result:
[Object]

Esto coincide con el resultado del video. ¿Que está pasando aqui? ¿Por qué es esto un objeto? ¿Qué hace el +operador?

Object + Empty Array
{} + []
result:
[Object]

Esto no coincide con el video. 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 la salida de una variable da como resultado dos objetos? Tal vez mi JSFiddle está mal.

Array(16).join("wat" - 1)
result:
NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN

Hacer wat + 1 da como resultado wat1wat1wat1wat1...

Sospecho que esto es solo un comportamiento directo que tratar de restar un número de una cadena da como resultado NaN.

NibblyPig
fuente
44
El {} + [] es básicamente el único complicado y dependiente de la implementación, como explico aquí , porque depende de ser analizado como una declaración o como una expresión. ¿En qué entorno está probando (obtuve el 0 esperado en Firefow y Chrome pero obtuve el "[objeto Objeto]" en NodeJs)?
hugomg
1
Estoy ejecutando Firefox 9.0.1 en Windows 7, y JSFiddle lo evalúa como [Objeto]
NibblyPig
@missingno me sale 0 en el NodeJS REPL
OrangeDog
41
Array(16).join("wat" - 1) + " Batman!"
Nick Johnson
1
@missingno publicó la pregunta aquí , pero para {} + {}.
Ionică Bizău

Respuestas:

1480

Aquí hay una lista de explicaciones de los resultados que está viendo (y se supone que está viendo). 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 , la conversión de un objeto (en este caso, una matriz) a una primitiva devuelve su valor predeterminado, que para los objetos con un toString()método válido es el resultado de la llamada object.toString()( §8.12.8 ). Para las 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.

  2. [] + {}

    Similar a [] + [], ambos operandos se convierten a primitivos primero. Para "Objetos de objeto" (§15.2), este es nuevamente el resultado de la llamada object.toString(), que para objetos no nulos y no definidos"[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 que no obligue a esa declaración a ser una expresión, pero más sobre 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 de acuerdo con §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 +{}, tenemos que aplicarToNumber en la cadena "[object Object]". Al seguir los pasos del §9.3.1 , obtenemos NaNcomo resultado:

    Si la gramática no puede interpretar el 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, §11.6.2 pasos # 5 y # 6 muestran que tenemos que convertir ambos operandos en un número usando ToNumber. ToNumber(1)es simplemente 1 ( §9.3 ), mientras que de ToNumber("wat")nuevo 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 de 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 la cadena "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á viendo. Cuando se usa en "wat" + 1lugar de "wat" - 1como argumento, el operador de suma convierte 1a una cadena en lugar de convertir "wat"a un número, por lo que efectivamente llama Array(16).join("wat1").

En cuanto a por qué estás viendo resultados diferentes para el {} + []caso: cuando lo usas como un argumento de función, estás forzando que la declaración sea un ExpressionStatement , lo que hace que sea imposible analizarlo {}como un bloque vacío, por lo que se analiza como un objeto vacío literal.

Ventero
fuente
2
Entonces, ¿por qué es [] +1 => "1" y [] -1 => -1?
Rob Elsner
44
@RobElsner []+1sigue la misma lógica que []+[], solo con el 1.toString()operando rhs. Para []-1ver la explicación del "wat"-1punto 5. Recuerde que ToNumber(ToPrimitive([]))es 0 (punto 3).
Ventero
44
Falta esta explicación / omite muchos detalles. Por ejemplo, "convertir un objeto (en este caso, una matriz) en una primitiva devuelve su valor predeterminado, que para los objetos con un método toString () válido es el resultado de llamar a object.toString ()" falta por completo que el valor de [] es llamado primero, pero debido a que el valor de retorno no es primitivo (es una matriz), en su lugar se usa toString de []. En su lugar, recomendaría mirar esto para obtener una explicación en profundidad real 2ality.com/2012/01/object-plus-object.html
jahav
30

Esto es más un comentario que una respuesta, pero por alguna razón no puedo comentar tu pregunta. Quería corregir su 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 el inicio de una línea de código). Entonces, cuando escribes, out({} + [])estás obligando {}a ser algo que no es cuando escribes{} + [] . Esto es parte del 'wat' ness general de Javascript.

La idea básica era que JavaScript simple quería permitir ambas formas:

if (u)
    v;

if (x) {
    y;
    z;
}

Para hacerlo, se hicieron dos interpretaciones de la llave de apertura: 1. no es obligatorio y 2. puede aparecer en cualquier lugar .

Este fue un movimiento equivocado. El código real no tiene una llave de apertura que aparece en el 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 rizado llaves. Eventualmente acabo de adoptar el hábito de que siempre se requieren llaves, incluso cuando solo escribes una línea).

Afortunadamente, en muchos casos eval () replicará el vatio completo 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>");

[También es la primera vez que escribo document.writeln en muchos, muchos años, y me siento un poco sucio escribiendo algo relacionado con document.writeln () y eval ().]

CR Drost
fuente
15
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere- No estoy de acuerdo (más o menos): A menudo tengo en los últimos bloques usados como este para variables de ámbito de C . Este hábito se retomó hace un tiempo cuando se hacía C incrustado, donde las variables en la pila ocupan espacio, por lo que si ya no son necesarias, queremos liberar el espacio al final del bloque. Sin embargo, ECMAScript solo abarca dentro de los bloques function () {}. Entonces, aunque no estoy de acuerdo con que el concepto sea incorrecto, estoy de acuerdo en que la implementación en JS es ( posiblemente ) incorrecta.
Jess Telford
44
@JessTelford En ES6, puede usar letpara declarar variables de ámbito de bloque.
Oriol
19

Secundo la solución de @ Ventero. Si lo desea, puede entrar en más detalles sobre cómo +convierte sus operandos.

El primer paso (§9.1): convertir ambos operandos a primitivas (valores primitivos son undefined, null, booleanos, números, cadenas; todos los demás valores son objetos, incluidas las matrices y funciones). Si un operando ya es primitivo, ya está. Si no, es un objeto objy se realizan los siguientes pasos:

  1. Llamada obj.valueOf(). Si devuelve un primitivo, ya está. Las instancias directas Objecty las matrices se devuelven, por lo que aún no ha terminado.
  2. Llamada obj.toString(). Si devuelve un primitivo, ya está. {}y[] ambos devuelven una cadena, así que ya está.
  3. De lo contrario, arroje 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 en primitivo y luego en 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 concatenando dos cadenas. De lo contrario, ambos operandos se convierten en números y el resultado se produce al agregarlos.

Explicación más detallada del proceso de conversión: " ¿Qué es {} + {} en JavaScript? "

Axel Rauschmayer
fuente
13

Podemos referirnos a la especificación y eso es genial y más 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 solo con valores primitivos. Más específicamente +(suma) funciona con cadenas o números, y +(unario) y -(resta y unario) solo funciona 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 realiza con valueOfo toString, que están disponibles en cualquier objeto. Esa es la razón por la cual tales funciones u operadores no arrojan errores cuando se invocan en objetos.

Entonces podemos decir que:

  • [] + []es lo mismo String([]) + String([])que lo mismo que '' + ''. Mencioné anteriormente que +(la suma) también es válida para los números, pero no hay una representación numérica válida de una matriz en JavaScript, por lo que se usa la adición de cadenas.
  • [] + {}es lo mismo String([]) + String({})que lo mismo que'' + '[object Object]'
  • {} + []. Esta merece más explicación (ver la 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 igual que +[]. Unary +solo funciona con números, por lo que la implementación intenta obtener un número []. Primero intenta lo valueOfque en el caso de las matrices devuelve el mismo objeto, luego intenta el último recurso: la conversión de un toStringresultado a un número. Podemos escribirlo como +Number(String([]))cual es igual a +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 "wat"que no se puede convertir a un número válido. Nos recibe NaN, y cualquier operación aritmética de NaNlos resultados con NaN, por lo que tenemos: Array(16).join(NaN).
Mariusz Nowak
fuente
0

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 hay dos posibles interpretaciones basadas en los tipos de operandos (int, string) y (int int):

  • El usuario tiene la intención de concatenar dos cadenas, resultado: "12"
  • El usuario tiene la intención de agregar dos números, resultado: 3

Por lo tanto, con diferentes tipos de entrada, aumentan las posibilidades de salida.

El algoritmo de suma

  1. Coaccionar operandos a valores primitivos

Las primitivas de JavaScript son string, number, null, undefined y boolean (el símbolo llegará pronto 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 (), devuelva este valor; de lo contrario, continúe

  • Lanzar un error de tipo

Nota: Para valores de fecha, el pedido es invocar toString antes de valueOf.

  1. Si cualquier valor de operando es una cadena, entonces realice una concatenación de cadena

  2. De lo contrario, convierta ambos operandos a su valor numérico y luego agregue estos valores

Conocer los diversos valores de coerción de los tipos en JavaScript ayuda a aclarar los resultados confusos. Ver 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 a la izquierda, ya que esto determina cuáles serán los resultados que involucren más de una operación +.

Aprovechando Así 1 + "2" dará "12" porque cualquier adición que involucre una cadena siempre tendrá por defecto la concatenación de cadenas.

Puede leer más ejemplos en esta publicación de blog (descargo de responsabilidad que lo escribí).

AbdulFattah Popoola
fuente