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


753

Le discours «Wat» pour 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 (comme 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 en arrière-plan dans chaque cas.

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

Je suis assez curieux de connaître l' +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 l' +opérateur?

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ésulte en wat1wat1wat1wat1...

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


4
Le {} + [] est fondamentalement le seul délicat et dépend de l'implémentation, comme je l'explique ici , car il dépend d'être analysé comme une instruction ou comme une expression. Dans quel environnement testez-vous (j'ai obtenu le 0 attendu dans Firefow et Chrome mais j'ai obtenu "[object object]" dans NodeJs)?
hugomg

1
J'utilise Firefox 9.0.1 sur Windows 7 et JSFiddle l'évalue en [Object]
NibblyPig

@missingno je reçois 0 dans le NodeJS REPL
OrangeDog

41
Array(16).join("wat" - 1) + " Batman!"
Nick Johnson

1
@missingno Posté la question ici , mais pour {} + {}.
Ionică Bizău

Réponses:


1480

Voici une liste d'explications pour les résultats que vous voyez (et êtes censés voir). Les références que j'utilise proviennent de la 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 ). Conformément au §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 une toString()méthode valide est le résultat de l'appel object.toString()( §8.12.8 ). Pour les tableaux, cela revient à appeler array.join()( §15.4.4.2 ). Rejoindre un tableau vide entraîne une chaîne vide, donc l'étape # 7 de l'opérateur d'addition renvoie la concaténation de deux chaînes vides, qui est la chaîne vide.

  2. [] + {}

    De manière similaire à [] + [], les deux opérandes sont d'abord convertis en primitives. Pour les "objets objets" (§15.2), c'est encore le résultat de l'appel object.toString(), ce qui est pour les objets non nuls, non indéfinis "[object Object]"( §15.2.4.2 ).

  3. {} + []

    L' {}ici n'est pas analysé comme un objet, mais plutôt comme un bloc vide ( §12.1 , au moins tant que vous ne forcez pas cette déclaration à ê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' +opérateur unaire ( §11.4.6 ) revient ToNumber(ToPrimitive(operand)). Comme nous le savons déjà, ToPrimitive([])est la chaîne vide, et selon §9.3.1 , ToNumber("")est 0.

  4. {} + {}

    Comme dans le cas précédent, le premier {}est analysé comme un bloc avec une valeur de retour vide. Encore une fois, +{}est le même que ToNumber(ToPrimitive({})), et ToPrimitive({})est "[object Object]"(voir [] + {}). Donc, pour obtenir le résultat de +{}, nous devons appliquer ToNumbersur la chaîne "[object Object]". En suivant les étapes du §9.3.1 , nous obtenons NaNcomme résultat:

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

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

    Conformément aux §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, les étapes # 5 et # 6 du § 11.6.2 montrent que nous devons convertir les deux opérandes en un nombre utilisant ToNumber. ToNumber(1)est simplement 1 ( §9.3 ), alors que c'est ToNumber("wat")encore NaNcomme au §9.3.1 . Après l'étape 7 du §11.6.2 , le §11.6.3 stipule que

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

    L'argument Array(16).joinest donc NaN. Après le §15.4.4.5 ( Array.prototype.join), nous devons invoquer ToStringl'argument, qui est "NaN"( §9.8.1 ):

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

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

Quant à savoir pourquoi vous voyez des résultats différents pour le {} + []cas: lorsque vous l'utilisez comme argument de fonction, vous forcez l'instruction à être un ExpressionStatement , ce qui rend impossible l'analyse en {}tant que bloc vide, elle est donc analysée en tant qu'objet vide littéral.


2
Alors pourquoi [] +1 => "1" et [] -1 => -1?
Rob Elsner

4
@RobElsner []+1suit à peu près la même logique que []+[], juste avec l' 1.toString()opérande as rhs. Pour []-1voir l'explication du "wat"-1point 5. N'oubliez pas que ToNumber(ToPrimitive([]))c'est 0 (point 3).
Ventero

4
Cette explication manque / omet beaucoup de détails. Par exemple, "la conversion d'un objet (dans ce cas, un tableau) en une primitive renvoie sa valeur par défaut qui, pour les objets avec une méthode toString () valide, est le résultat de l'appel de object.toString ()", il manque complètement cette valeurOf de [] est appelé en premier, mais comme la valeur de retour n'est pas une primitive (c'est un tableau), la toString de [] est utilisée à la place. Je recommanderais de regarder ceci à la place pour une véritable explication approfondie 2ality.com/2012/01/object-plus-object.html
jahav

30

Il s'agit plus d'un commentaire que d'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é cela sur Hacker News et quelqu'un m'a suggéré de le republier ici.

Le problème dans le code JSFiddle est que ({})(les accolades ouvrantes à l'intérieur des parenthèses) ne sont pas les mêmes que {}(les accolades ouvrantes comme le début d'une ligne de code). Donc, lorsque vous tapez, out({} + [])vous forcez {}à être quelque chose qui ne l'est pas lorsque vous tapez {} + []. Cela fait partie de la «wat'-ness» globale de Javascript.

L'idée de base était que JavaScript voulait simplement autoriser ces deux formes:

if (u)
    v;

if (x) {
    y;
    z;
}

Pour ce faire, deux interprétations ont été faites de l'accolade ouvrante: 1. elle n'est pas obligatoire et 2. elle peut apparaître n'importe où .

C'était une mauvaise décision. Le code réel n'a pas d'accolade ouvrante apparaissant au milieu de nulle part, et le code réel a également tendance à être plus fragile lorsqu'il utilise le premier formulaire plutôt que le second. (Environ une fois tous les deux mois à mon dernier emploi, je recevais un appel au bureau d'un collègue lorsque leurs modifications à mon code ne fonctionnaient pas, et le problème était qu'ils avaient ajouté une ligne au "si" sans ajouter de bouclé J'ai finalement pris l'habitude que les accolades soient toujours obligatoires, même lorsque vous n'écrivez qu'une seule ligne.)

Heureusement, dans de nombreux cas, eval () reproduira l'intégralité de l'eau de JavaScript. Le code JSFiddle devrait se 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>");

[C'est aussi la première fois que j'écris document.writeln depuis de nombreuses années, et je me sens un peu sale en écrivant quoi que ce soit impliquant à la fois document.writeln () et eval ().]


15
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere- je suis en désaccord ( un peu): J'ai souvent dans les blocs utilisés passé comme celui - ci à des variables de portée en C . Cette habitude a été reprise il y a quelque temps lors de l'exécution de C intégré où les variables de la pile prennent de l'espace, donc si elles ne sont plus nécessaires, nous voulons que l'espace soit libéré à la fin du bloc. Cependant, ECMAScript n'étend que dans les blocs function () {}. Donc, bien que je ne sois pas d'accord que le concept soit erroné, je conviens que l'implémentation dans JS est ( peut-être ) erronée.
Jess Telford

4
@JessTelford Dans ES6, vous pouvez utiliser letpour déclarer des variables de portée bloc.
Oriol

19

J'appuie la solution de @ Ventero. Si vous le souhaitez, vous pouvez entrer plus en détail sur la façon de +convertir ses opérandes.

Première étape (§9.1): convertir les deux opérandes de primitives (valeurs primitives sont undefined, null, booléens, nombres, des chaînes, 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 objet les étapes suivantes sont effectuées:

  1. Appelle obj.valueOf(). S'il renvoie une primitive, vous avez terminé. Les instances directes Objectet les tableaux se renvoient eux-mêmes, vous n'avez donc pas encore terminé.
  2. Appelle obj.toString(). S'il renvoie une primitive, vous avez terminé. {}et les []deux renvoient une chaîne, vous avez donc terminé.
  3. Sinon, lancez a 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? "


13

Nous pouvons nous référer à la spécification et c'est génial et le plus précis, mais la plupart des cas peuvent également être expliqués de manière plus compréhensible avec les déclarations suivantes:

  • +et les -opérateurs ne fonctionnent qu'avec des valeurs primitives. Plus précisément +(l'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 comme argument convertissent d'abord cet argument en type primitif souhaité. Cela se fait avec valueOfou toString, qui sont disponibles sur n'importe quel objet. C'est la raison pour laquelle de telles fonctions ou opérateurs ne génèrent pas d'erreurs lorsqu'ils sont invoqués sur des objets.

On peut donc dire que:

  • [] + []est identique à String([]) + String([])ce qui est identique à '' + ''. J'ai mentionné ci-dessus que +(l'addition) est également valable 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 celui 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 ne sont pas traitées comme un objet mais comme un bloc vide, il se révèle donc être le même que +[]. Unary +ne fonctionne qu'avec des nombres, donc l'implémentation essaie d'en extraire un nombre []. Il essaie d'abord valueOfqui dans le cas des tableaux renvoie le même objet, puis il essaie le dernier recours: conversion d'un toStringrésultat en un nombre. Nous pouvons l'écrire comme +Number(String([]))ce qui est identique à +Number('')ce qui est identique à +0.
  • Array(16).join("wat" - 1)la soustraction -ne fonctionne qu'avec des nombres, c'est donc la même chose que:, Array(16).join(Number("wat") - 1)as "wat"ne peut pas être converti en un nombre valide. Nous recevons NaN, et toute opération arithmétique sur les NaNrésultats avec NaN, nous avons donc: Array(16).join(NaN).

0

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ë car il existe deux interprétations possibles basées sur les types d'opérandes (int, chaîne) 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 nombres, résultat: 3

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

L'algorithme d'addition

  1. Contrainte d'opérandes à des valeurs primitives

Les primitives JavaScript sont chaîne, nombre, null, non défini et booléen (le symbole arrivera 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 retournée lorsque object.valueOf () est invoquée, retournez cette valeur, sinon continuez

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

  • Lancer une erreur TypeError

Remarque: Pour les valeurs de date, l'ordre est d'appeler 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 déroutantes plus claires. 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 cela détermine quels seront les cas de sortie impliquant plus d'une opération +.

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

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.