Cet article est un hommage au fantastique discours « Wat » de Gary Bernhardt dans lequel il souligne les particularités de certaines constructions de langage dans Ruby et JavaScript. Si vous n’avez pas encore regardé la conférence, je vous recommande fortement de prendre le temps et de le faire précisément! Ce n’est qu’environ 4 minutes et très divertissant, je vous le promets.
Dans son exposé, Gary montre ces quatre fragments de code JavaScript:
Nous voyons beaucoup de crochets, d’accolades et de signes plus. Voici ce que ces fragments évaluent:
+ == ""
+ {} == ""
{} + == 0
{} + {} == NaN
Quand j’ai vu ces exemples pour la première fois, je me suis dit: « Wow, ça a l’air désordonné! »Les résultats peuvent sembler incohérents ou même arbitraires, mais supportez-moi ici. Tous ces exemples sont en fait très cohérents et pas aussi mauvais qu’ils en ont l’air!
# Fragment #1: +
Commençons par le premier fragment:
+ // ""
Comme nous pouvons le voir, l’application de l’opérateur +
à deux tableaux vides entraîne une chaîne vide. En effet, la représentation sous forme de chaîne d’un tableau est la représentation sous forme de chaîne de tous ses éléments, concaténés avec des virgules:
.toString()// "1,2,3".toString()// "1,2".toString()// "1".toString()// ""
Un tableau vide ne contient aucun élément, donc sa représentation sous forme de chaîne est une chaîne vide. Par conséquent, la concaténation de deux chaînes vides n’est qu’une autre chaîne vide.
# Fragment #2: + {}
Jusqu’ici, tout va bien. Examinons maintenant le deuxième fragment:
+ {}// ""
Notez que parce que nous n’avons pas affaire à deux nombres, l’opérateur +
effectue à nouveau la concaténation de chaînes plutôt que l’addition de deux valeurs numériques.
Dans la section précédente, nous avons déjà vu que la représentation sous forme de chaîne d’un tableau vide est une chaîne vide. La représentation sous forme de chaîne du littéral d’objet vide est ici la valeur par défaut ""
. L’ajout d’une chaîne vide ne change pas la valeur, donc ""
est le résultat final.
En JavaScript, les objets peuvent implémenter une méthode spéciale appelée toString()
qui renvoie une représentation sous forme de chaîne personnalisée de l’objet sur lequel la méthode est appelée. Notre littéral d’objet vide n’implémente pas une telle méthode, nous revenons donc à l’implémentation par défaut du prototype Object
.
# Fragment #3: {} +
Je dirais que jusqu’à présent, les résultats n’ont pas été trop inattendus. Ils ont simplement suivi les règles de coercition de type et de représentations de chaînes par défaut en JavaScript.
Cependant, {} +
est l’endroit où les développeurs commencent à se confondre:
{} + // 0
Pourquoi voyons-nous 0
(le nombre zéro) si nous tapons la ligne ci-dessus dans un REPL JavaScript comme la console du navigateur? Le résultat ne devrait-il pas être une chaîne, tout comme l’était + {}
?
Avant de résoudre l’énigme, considérez les trois façons différentes d’utiliser l’opérateur +
:
// 1) Addition of two numeric values2 + 2 == 4// 2) String concatenation of two values"2" + "2" == "22"// 3) Conversion of a value to a number+2 == 2+"2" == 2
Dans les deux premiers cas, l’opérateur +
est un opérateur binaire car il possède deux opérandes (à gauche et à droite). Dans le troisième cas, l’opérateur +
est un opérateur unaire car il n’a qu’un seul opérande (à droite).
Considérez également les deux significations possibles de {}
en JavaScript. Habituellement, nous écrivons {}
pour signifier un littéral d’objet vide, mais si nous sommes en position d’instruction, la grammaire JavaScript spécifie {}
pour signifier un bloc vide. Le morceau de code suivant définit deux blocs vides, dont aucun n’est un littéral d’objet:
{}// Empty block{ // Empty block}
Jetons à nouveau un coup d’œil à notre fragment:
{} +
Permettez-moi de changer un peu les espaces pour clarifier la façon dont le moteur JavaScript voit le code:
{ // Empty block}+;
Maintenant, nous pouvons voir clairement ce qui se passe ici. Nous avons une instruction block suivie d’une autre instruction qui contient une expression unaire +
fonctionnant sur un tableau vide. Le point-virgule de fin est inséré automatiquement selon les règles de l’ASI (insertion automatique de point-virgule).
Vous pouvez facilement vérifier dans la console de votre navigateur que +
est évalué à 0
. Le tableau vide a une chaîne vide comme représentation de chaîne, qui à son tour est convertie en nombre zéro par l’opérateur +
. Enfin, la valeur de la dernière instruction (+
, dans ce cas) est signalée par la console du navigateur.
Alternativement, vous pouvez alimenter les deux extraits de code à un analyseur JavaScript tel qu’Esprima et comparer les arbres de syntaxe abstraits résultants. Voici l’AST pour + {}
:
{ "type": "Program", "body": }, "right": { "type": "ObjectExpression", "properties": } } } ], "sourceType": "script"}
Et voici l’AST pour {} +
:
{ "type": "Program", "body": }, { "type": "ExpressionStatement", "expression": { "type": "UnaryExpression", "operator": "+", "argument": { "type": "ArrayExpression", "elements": }, "prefix": true } } ], "sourceType": "script"}
La confusion provient d’une nuance de la grammaire JavaScript qui utilise des accolades à la fois pour les littéraux d’objets et les blocs. En position d’instruction, une accolade d’ouverture démarre un bloc, tandis qu’en position d’expression, une accolade d’ouverture démarre un littéral d’objet.
# Fragment #4: {} + {}
Enfin, jetons rapidement un coup d’œil à notre dernier fragment {} + {}
:
{} + {}// NaN
Eh bien, ajouter deux littéraux d’objets n’est littéralement « pas un nombre » — mais ajoutons-nous deux littéraux d’objets ici? Ne laissez plus les bretelles vous tromper! C’est ce qui se passe:
{ // Empty block}+{};
C’est à peu près la même affaire que dans l’exemple précédent. Cependant, nous appliquons maintenant l’opérateur unaire plus à un littéral d’objet vide. C’est fondamentalement la même chose que faire Number({})
, ce qui entraîne NaN
car notre littéral d’objet ne peut pas être converti en nombre.
Si vous souhaitez que le moteur JavaScript analyse le code sous la forme de deux littéraux d’objets vides, enveloppez le premier (ou le morceau de code entier) entre parenthèses. Vous devriez maintenant voir le résultat attendu:
({}) + {}// ""({} + {})// ""
La parenthèse d’ouverture amène l’analyseur à tenter de reconnaître une expression, c’est pourquoi il ne traite pas le {}
comme un bloc (qui serait une instruction).
# Summary
Vous devriez maintenant voir pourquoi les quatre fragments de code évaluent comme ils le font. Ce n’est pas du tout arbitraire ou aléatoire; les règles de coercition de type sont appliquées exactement comme prévu dans la spécification et la grammaire du langage.
Gardez simplement à l’esprit que si une accolade d’ouverture est le premier caractère à apparaître dans une instruction, elle sera interprétée comme le début d’un bloc plutôt qu’un littéral d’objet.