Question Quelle est l'explication de ces comportements JavaScript bizarres mentionnés dans le discours 'Wat' pour CodeMash 2012?


le 'Wat' parle de CodeMash 2012 souligne essentiellement quelques bizarreries bizarres avec Ruby et JavaScript.

J'ai fait un JSFiddle des résultats à http://jsfiddle.net/fe479/9/.

Les comportements spécifiques à JavaScript (car je ne connais pas Ruby) sont listés ci-dessous.

J'ai trouvé dans le JSFiddle que certains de mes résultats ne correspondaient pas à ceux de la vidéo, et je ne sais pas pourquoi. Je suis, cependant, curieux de savoir comment JavaScript gère le travail dans les coulisses dans chaque cas.

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

Je suis assez curieux de savoir + opérateur lorsqu'il est utilisé avec des tableaux en JavaScript. Cela correspond au résultat de la vidéo.

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

Cela correspond au résultat de la vidéo. Que se passe t-il ici? Pourquoi est-ce un objet. Que fait le + opérateur faire?

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

Cela ne correspond pas à la vidéo. La vidéo suggère que le résultat est 0, alors que j'obtiens [Object].

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

Cela ne correspond pas non plus à la vidéo, et comment la sortie d'une variable entraîne-t-elle deux objets? Peut-être que mon JSFiddle est faux.

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

Faire wat + 1 résultats à wat1wat1wat1wat1...

Je soupçonne que c'est simplement un comportement direct que d'essayer de soustraire un nombre d'une chaîne entraîne NaN.


702
2018-01-27 11:49


origine


Réponses:


Voici une liste d'explications pour les résultats que vous voyez (et supposés voir). Les références que j'utilise proviennent du Norme ECMA-262.

  1. [] + []

    Lors de l'utilisation de l'opérateur d'addition, les opérandes gauche et droit sont d'abord convertis en primitives (§11.6.1). Selon §9.1, la conversion d'un objet (dans ce cas un tableau) en une primitive renvoie sa valeur par défaut, qui pour les objets avec toString() méthode est le résultat de l'appel object.toString() (§8.12.8). Pour les tableaux, cela revient à appeler array.join() (§15.4.4.2). La jointure d'un tableau vide génère une chaîne vide. L'étape n ° 7 de l'opérateur d'addition renvoie la concaténation de deux chaînes vides, à savoir la chaîne vide.

  2. [] + {}

    Semblable à [] + [], les deux opérandes sont convertis en primitives en premier. Pour "Objets objet" (§15.2), ceci est encore le résultat d'un appel object.toString(), qui pour les objets non-null, non-indéfinis est "[object Object]" (§15.2.4.2).

  3. {} + []

    le {} ici n'est pas analysé comme un objet, mais plutôt comme un bloc vide (§12.1, du moins tant que vous ne forcerz pas cette expression à être une expression, mais plus à ce sujet plus tard). La valeur de retour des blocs vides est vide, donc le résultat de cette instruction est le même que +[]. L'unaire + opérateur (§11.4.6) résultats ToNumber(ToPrimitive(operand)). Comme nous le savons déjà, ToPrimitive([]) est la chaîne vide, et selon §9.3.1, ToNumber("") est 0

  4. {} + {}

    Similaire au cas précédent, le premier {} est analysé comme un bloc avec une valeur de retour vide. Encore, +{} est le même que ToNumber(ToPrimitive({})), et ToPrimitive({}) est "[object Object]" (voir [] + {}). Donc, pour obtenir le résultat de +{}, nous devons appliquer ToNumbersur le fil "[object Object]". En suivant les étapes de §9.3.1, on a NaN Par conséquent:

    Si la grammaire ne peut pas interpréter la chaîne comme une extension de StringNumericLiteral, puis le résultat de ToNumber est NaN.

  5. Array(16).join("wat" - 1)

    Selon §15.4.1.1 et §15.4.2.2, Array(16) crée un nouveau tableau de longueur 16. Pour obtenir la valeur de l'argument à joindre, §11.6.2 étapes # 5 et # 6 montrent que nous devons convertir les deux opérandes en un nombre en utilisant ToNumber. ToNumber(1) est simplement 1 (§9.3), tandis que ToNumber("wat") est encore NaN selon §9.3.1. Après l'étape 7 de §11.6.2, §11.6.3 dicte que

    Si l'un des opérandes est NaN, le résultat est NaN.

    Donc, l'argument de Array(16).join est NaN. Suivant le §15.4.4.5 (Array.prototype.join), nous devons appeler ToString sur l'argument, qui est "NaN" (§9.8.1):

    Si m est NaN, retourne la chaîne "NaN".

    Après l'étape 10 de §15.4.4.5, nous obtenons 15 répétitions de la concaténation de "NaN" et la chaîne vide, ce qui équivaut au résultat que vous voyez. En utilisant "wat" + 1 au lieu de "wat" - 1 comme argument, l'opérateur d'addition convertit 1 à une chaîne au lieu de convertir "wat" à un certain nombre, de sorte qu'il appelle efficacement Array(16).join("wat1").

Quant à savoir pourquoi vous voyez des résultats différents pour le {} + [] case: En l'utilisant comme argument de fonction, vous forcez l'instruction à être un ExpressionStatement, ce qui rend impossible d'analyser {} comme un bloc vide, il est donc analysé à la place comme un objet littéral vide.


1417
2018-01-27 12:33



C'est plus un commentaire qu'une réponse, mais pour une raison quelconque je ne peux pas commenter votre question. Je voulais corriger votre code JSFiddle. Cependant, j'ai posté ceci sur Hacker News et quelqu'un a suggéré que je le repost ici.

Le problème dans le code JSFiddle est que ({}) (ouverture des accolades à l'intérieur des parenthèses) n'est pas la même chose que {} (ouverture des accolades comme le début d'une ligne de code). Alors quand vous tapez out({} + []) vous forcer le {} être quelque chose que ce n'est pas quand vous tapez {} + []. Cela fait partie de la globalité de Javascript.

L'idée de base était simple JavaScript voulait permettre à ces deux formes:

if (u)
    v;

if (x) {
    y;
    z;
}

Pour ce faire, deux interprétations ont été faites de l'accolade d'ouverture: 1. il est non requis et 2. il peut apparaître nulle part.

C'était un mauvais mouvement. Le vrai code n'a pas d'accolade d'ouverture apparaissant au milieu de nulle part, et le vrai code a aussi tendance à être plus fragile quand il utilise la première forme plutôt que la seconde. (Environ une fois tous les deux mois lors de mon dernier travail, je serais appelé au bureau d'un collègue lorsque leurs modifications de mon code ne fonctionnaient pas, et le problème était qu'ils avaient ajouté une ligne au "si" sans ajouter bouclés J'ai fini par adopter l'habitude que les accolades soient toujours nécessaires, même quand vous écrivez seulement une ligne.)

Heureusement, dans de nombreux cas, eval () va répliquer l'intégralité de JavaScript. Le code JSFiddle devrait lire:

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>");

[Aussi, c'est la première fois que j'ai écrit document.writeln depuis de nombreuses années, et je me sens un peu sale en train d'écrire quoi que ce soit impliquant à la fois document.writeln () et eval ().]


28
2018-01-30 18:40



Je seconde la solution de Ventero. Si vous voulez, vous pouvez entrer dans plus de détails sur la façon dont + convertit ses opérandes.

Première étape (§9.1): convertir les deux opérandes en primitives (les valeurs primitives sont undefined, null, les booléens, les nombres, les cordes; toutes les autres valeurs sont des objets, y compris des tableaux et des fonctions). Si un opérande est déjà primitif, vous avez terminé. Sinon, c'est un objet obj et les étapes suivantes sont effectuées:

  1. Appel obj.valueOf(). Si elle renvoie une primitive, vous avez terminé. Instances directes de Object et les tableaux retournent eux-mêmes, donc vous n'avez pas encore fini.
  2. Appel obj.toString(). Si elle renvoie une primitive, vous avez terminé. {} et [] les deux retournent une chaîne, donc vous avez terminé.
  3. Sinon, lancez un TypeError.

Pour les dates, les étapes 1 et 2 sont permutées. Vous pouvez observer le comportement de conversion comme suit:

var obj = {
    valueOf: function () {
        console.log("valueOf");
        return {}; // not a primitive
    },
    toString: function () {
        console.log("toString");
        return {}; // not a primitive
    }
}

Interaction (Number() convertit d'abord en primitif puis en nombre):

> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value

Deuxième étape (§11.6.1): Si l'un des opérandes est une chaîne, l'autre opérande est également converti en chaîne et le résultat est produit en concaténant deux chaînes. Sinon, les deux opérandes sont convertis en nombres et le résultat est produit en les ajoutant.

Explication plus détaillée du processus de conversion: "Qu'est-ce que {} + {} en JavaScript?"


16
2018-01-30 04:57



Nous pouvons nous référer à la spécification et c'est très bien et très précis, mais la plupart des cas peuvent également être expliqués de manière plus compréhensible avec les énoncés suivants:

  • + et - les opérateurs travaillent uniquement avec des valeurs primitives. Plus précisement +(addition) fonctionne avec des chaînes ou des nombres, et +(unaire) et -(soustraction et unaire) ne fonctionne qu'avec des nombres.
  • Toutes les fonctions ou opérateurs natifs qui attendent une valeur primitive en tant qu'argument convertissent d'abord cet argument en type primitif souhaité. C'est fait avec valueOf ou toString, qui sont disponibles sur n'importe quel objet. C'est la raison pour laquelle de telles fonctions ou opérateurs ne lancent pas d'erreurs lorsqu'ils sont invoqués sur des objets.

Nous pouvons donc dire que:

  • [] + [] est le même que String([]) + String([]) qui est le même que '' + ''. J'ai mentionné plus haut que +(addition) est également valide pour les nombres, mais il n'y a pas de représentation numérique valide d'un tableau en JavaScript, donc l'ajout de chaînes est utilisé à la place.
  • [] + {} est le même que String([]) + String({}) qui est le même que '' + '[object Object]'
  • {} + []. Celui-ci mérite plus d'explications (voir la réponse de Ventero). Dans ce cas, les accolades sont traitées non comme un objet mais comme un bloc vide, il s'avère donc être identique à +[]. Unaire + fonctionne uniquement avec des nombres, de sorte que la mise en œuvre essaie d'obtenir un nombre de []. D'abord, il essaie valueOf qui dans le cas de tableaux renvoie le même objet, alors il essaie le dernier recours: la conversion d'un toString résultat à un nombre. Nous pouvons l'écrire comme +Number(String([])) qui est le même que +Number('') qui est le même que +0.
  • Array(16).join("wat" - 1) soustraction - fonctionne uniquement avec des chiffres, donc c'est pareil que: Array(16).join(Number("wat") - 1), comme "wat" ne peut pas être converti en un nombre valide. Nous recevons NaN, et toute opération arithmétique sur NaN résultats avec NaN, donc nous avons: Array(16).join(NaN).

13
2018-01-30 11:31



Pour étayer ce qui a été partagé plus tôt.

La cause sous-jacente de ce comportement est en partie due à la nature faiblement typée de JavaScript. Par exemple, l'expression 1 + "2" est ambiguë puisqu'il existe deux interprétations possibles basées sur les types d'opérande (int, string) et (int int):

  • L'utilisateur a l'intention de concaténer deux chaînes, résultat: "12"
  • L'utilisateur a l'intention d'ajouter deux chiffres, résultat: 3

Ainsi, avec des types d'entrée variables, les possibilités de sortie augmentent.

L'algorithme d'addition

  1. Coercer les opérandes aux valeurs primitives

Les primitives JavaScript sont string, number, null, undefined et boolean (le symbole arrive bientôt dans ES6). Toute autre valeur est un objet (par exemple des tableaux, des fonctions et des objets). Le processus de coercition pour convertir des objets en valeurs primitives est décrit ainsi:

  • Si une valeur primitive est renvoyée lorsque object.valueOf () est appelée, renvoyez cette valeur, sinon continuez

  • Si une valeur primitive est renvoyée lorsque object.toString () est invoqué, renvoyez cette valeur, sinon continuez

  • Lancer un TypeError

Remarque: Pour les valeurs de date, l'ordre est d'invoquer toString avant valueOf.

  1. Si une valeur d'opérande est une chaîne, effectuez une concaténation de chaîne

  2. Sinon, convertissez les deux opérandes en leur valeur numérique, puis ajoutez ces valeurs

Connaître les différentes valeurs de coercition des types en JavaScript aide à rendre les sorties plus confuses. Voir le tableau de coercition ci-dessous

+-----------------+-------------------+---------------+
| 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           |
+-----------------+-------------------+---------------+

Il est également bon de savoir que l'opérateur + de JavaScript est associatif à gauche car il détermine ce que seront les résultats impliquant plus d'une opération +.

Tirer parti de la Ainsi 1 + "2" donnera "12" parce que tout ajout impliquant une chaîne sera toujours par défaut à la concaténation de chaîne.

Vous pouvez lire plus d'exemples dans ce blog (avertissement je l'ai écrit).


0
2017-10-04 16:42