Une liste d'exemples JavaScript drôles et délicats
Le JavaScript est un langage formidable! Il possède une syntaxe simple, un grand écosystème et, le plus important de tout, une immense communauté.
En même temps, nous savous tous que le JavaScript est un langage assez amusant comprenant des aspects plus complexes que d'autres. Certains d'entre eux peuvent rapidement faire de notre travail quotidien un enfer, tout comme d'autres peuvent nous faire rire aux éclats.
L'idée originale de WTFJS appartient à Brian Leroux. Cette liste est fortement inspirée par son discours “WTFJS” at dotJS 2012:
Vous pouvez installer ce manuel en utilisant npm
. Pour cela, il suffit d'exécuter :
$ npm install -g wtfjs
Vous devriez pouvoir ensuite utiliser wtfjs
en ligne de commande. Cela ouvrira le manuel dans votre terminal. Sinon, vous pouvez continuer à lire ici tout simplement.
La source du package est disponible ici: https://github.com/denysdovhan/wtfjs
Actuellement, il existe des traductions de ** wtfjs ** pour les langues suivantes :
- 💪🏻 Motivation
- ✍🏻 Notation
- 👀 Exemples
[]
est égal à![]
true
n'est pas égal à![]
, mais pas égal à[]
aussi- true est faux
- baNaNa
NaN
n'est pas unNaN
- C'est un échec
[]
est truthy, mais pastrue
null
est falsy, mais pasfaux
document.all
est un objet, mais il estundefined
- La valeur minimale est supérieure à zéro
- Fonction n'est pas une fonction
- Ajout de tableaux
- Les virgules finales dans un tableau
- L'égalité des tableaux est un monstre
undefined
etNumber
parseInt
est un méchant- Math avec
true
etfalse
- Les commentaires HTML sont valides en JavaScript
NaN
n'estpasun nombre[]
etnull
sont des objets- Nombres magiquement croissant
- Précision de
0.1 + 0.2
- Patching de numéros
- Comparaison de trois nombres
- Math drôle
- Addition de RegExps
- Les chaînes ne sont pas des instances de
String
- Appeler des fonctions avec des caractères accent grave
- Call call call
- Une propriété
constructor
- Object en tant que clé de la propriété d'un objet
- Accéder aux prototypes avec
__proto__
`${{Object}}`
- Déstructuration avec des valeurs par défaut
- Points et propagation
- Étiquettes
- Étiquettes imbriquées
Try...catch
insidieux- Est-ce un héritage multiple ?
- Un générateur qui se
yield
lui-même - Une classe de classe
- Objets incoercibles
- Fonctions fléchées complexes
- Les fonctions fléchées ne peuvent pas être un constructeur
arguments
et fonctions fléchées- Retour difficile
- Chaînage d'affectations sur un objet
- Accéder aux propriétés d'un objet avec des tableaux
- Opérateurs
null
et relationnels Number.toFixed()
affiche différents nombresMath.max()
est moins queMath.min()
- Comparer
null
à0
- Même redéclaration d'une variable
- Comportement par défaut d'
Array.prototype.sort()
- 📚 Autres ressources
- 🎓 Licence
Juste pour le fun
— “Just for Fun: The Story of an Accidental Revolutionary”, Linus Torvalds
L'objectif principal de cette liste est de rassembler quelques exemples loufoques et d'expliquer leur fonctionnement, quand c'est possible. 😉 Tout simplement parce qu'il est amusant d'apprendre quelque chose qu'on ne connaissait pas auparavant.
Si vous êtes débutant, vous pouvez aussi utiliser ces notes pour approfondir vos connaissances en JavaScript. J'espère qu'elles vous inciteront à passer plus de temps à lire la spécification.
Si vous êtes un développeur professionnel, vous pouvez considérer ces exemples comme une excellente référence pour toutes les bizarreries et comportements inattendus de notre langage bien-aimé, le JavaScript.
Dans tous les cas, lisez ce qui suit. Vous y trouverez probablement quelque chose de nouveau !
// ->
est utilisé pour afficher le résultat d'une expression. Par exemple :
1 + 1; // -> 2
// >
définit le résultat de console.log
ou tout autre sortie. Par exemple :
console.log("hello, world!"); // > hello, world!
//
indique un commentaire utilisé pour donner des explications. Par exemple :
// Assigner une fonction la constant foo
const foo = function() {};
Tableau est égal à pas tableau
[] == ![]; // -> true
L'opérateur de comparaison d'égalité faible convertit les deux côtés en nombres pour les comparer. Pour différentes raisons, les deux côtés deviennent le nombre 0
.
Les tableaux sont truthy, donc à droite, on trouve l'opposé d'une valeur truthy, soit false
, qui est ensuite forcé à 0
. A gauche, puisqu'un tableau vide est forcé à 0
automatiquement, sans devoir être précédemment transformé en booléen, on trouve aussi 0
, malgré le fait qu'un tableau soit truthy.
Voici comment cette expression se simplifie:
+[] == +![];
0 == +false;
0 == 0;
true;
Voir aussi []
est truthy, mais pas true
.
Un tableau n'est pas égal à true
, tout comme pas tableau. Un tableau est égal à false
, pas tableau est égal à false
aussi :
true == []; // -> false
true == ![]; // -> false
false == []; // -> true
false == ![]; // -> true
true == []; // -> false
true == ![]; // -> false
// Selon la spécification
true == []; // -> false
toNumber(true); // -> 1
toNumber([]); // -> 0
1 == 0; // -> false
true == ![]; // -> false
![]; // -> false
true == false; // -> false
false == []; // -> true
false == ![]; // -> true
// Selon la spécification
false == []; // -> true
toNumber(false); // -> 0
toNumber([]); // -> 0
0 == 0; // -> true
false == ![]; // -> false
![]; // -> false
false == false; // -> true
!!"false" == !!"true"; // -> true
!!"false" === !!"true"; // -> true
Considérez ceci étape par étape :
// `true` est 'truthy' et est représenté par la valeur 1 (nombre), 'true' sous forme de chaîne est NaN.
true == "true"; // -> false
false == "false"; // -> false
// 'false' n'est pas une chaîne vide, donc c'est une valeur `truthy`
!!"false"; // -> true
!!"true"; // -> true
"b" + "a" + +"a" + "a"; // -> 'baNaNa'
Ceci est une blague "old school" en JavaScript, mais remasterisée. Voici l'originale :
"foo" + +"bar"; // -> 'fooNaN'
L'expression est évaluée comme 'foo' + (+'bar')
, ce qui convertit 'bar'
à NaN
.
NaN === NaN; // -> false
La spécification définit strictement la logique derrière ce comportement :
- Si
Type(x)
est différent deType(y)
, retourne false.- Si
Type(x)
estNumber
, alors
- Si
x
est NaN, retourne false.- Si
y
est NaN, retourne false.- … … …
Sur la base de la définition de NaN
de l'IEEE :
Quatre relations mutuellement exclusives sont possibles : inférieur à, égal, supérieur à, et non ordonné. Le dernier cas survient quand au moins un opérande est
NaN
. Tous lesNaN
doivent se comparer de manière non ordonnée avec tout, y compris avec lui-même.— “What is the rationale for all comparisons returning false for IEEE754 NaN values?” sur StackOverflow.
Vous ne le croiriez pas, mais …
(![] + [])[+[]] +
(![] + [])[+!+[]] +
([![]] + [][[]])[+!+[] + [+[]]] +
(![] + [])[!+[] + !+[]];
// -> 'fail'
En brisant cette masse de symboles en morceaux, nous remarquons que le schéma suivant se produit souvent :
![] + []; // -> 'false'
![]; // -> false
Donc, nous essayons d'ajouter []
à false
, mais en raison d'un certain nombre d'appels de fonctions internes (binary + Operator
-> ToPrimitive
-> [[DefaultValue]]
), nous finissons par convertir l'opérande de droite en chaîne :
![] + [].toString(); // 'false'
En considérant une chaîne comme un tableau, nous pouvons accéder à son premier caractère via [0]
:
"false"[0]; // -> 'f'
Le reste est évident, sauf pour le i
. Le i
dans fail
est saisi en générant la chaîne "falseundefined"
et en saisissant l'éléments sur l'index [10]
.
Un tableau est une valeur truthy
, mais n'est pas égal à true
.
!![] // -> true
[] == true // -> false
Voici des liens vers les sections correspondantes de la spécification ECMA-262 :
Malgré le fait que null
soit une valeur falsy
, elle n'est pas égale à false
.
0 == false; // -> true
"" == false; // -> true
L'explication est la même que pour l'exemple précédent. Voici le lien correspondant :
⚠️ Ceci fait partie de la Browser API et ne fonctionnera pas dans un environnement Node.js⚠️
Malgré le fait que document.all
soit un objet de type tableau et qu'il donne accès aux nœuds DOM de la page, il répond à la fonction typeof
comme étant undefined
.
document.all instanceof Object; // -> true
typeof document.all; // -> 'undefined'
En même temps, document.all
n'est pas égal à undefined
.
document.all === undefined; // -> false
document.all === null; // -> false
Mais, parallèlement :
document.all == null; // -> true
document.all
était anciennement un moyen d'accéder aux éléments DOM, principalement avec les anciennes versions d'IE. Bien que cela n'ait jamais été une norme,document.all
était largement utilisé dans "l'ancien code JS". Quand la norme a progressé avec la venue de nouvelles API (par exemple,document.getElementById
), l'APIdocument.all
est devenue obsolète et le comité de normes a dû décider ce qu'ils allaient en faire. En raison de sa large utilisation, ils ont décidé de la conserver, mais d'introduire une violation volontaire de la spécification JavaScript. La raison pour laquelledocument.all
retournefalse
lors de l'utilisation de l'opérateur d'égalité stricte (Strict Equality Comparison) avecundefined
ettrue
lors de l'utilisation de l'opérateur d'égalité abstraite (Abstract Equality Comparison) est due à la violation volontaire de la spécification qui le permet explicitement.— “Obsolete features - document.all” sur WhatWG - HTML spec. — “Chapter 4 - ToBoolean - Falsy values” sur YDKJS - Types & Grammar.
Number.MIN_VALUE
est le plus petit nombre, qui est supérieur à zéro :
Number.MIN_VALUE > 0; // -> true
Number.MIN_VALUE
est5e-324
, c'est-à-dire le plus petit nombre positif pouvant être représenté dans la précision flottante et donc, c'est aussi le plus près que possible de zéro. Il définit la meilleure résolution que vous pouvez atteindre avec des valeurs flottantes.Maintenant, la plus petite valeur globale est
Number.NEGATIVE_INFINITY
, bien que cette dernière ne soit pas vraiment numérique au sens strict.— “Why is
0
less thanNumber.MIN_VALUE
in JavaScript?” at StackOverflow
⚠️ Une erreur présente dans la v5.5 (V8) ou inférieure de Node.js (Node.js <=7)⚠️
Vous êtes tous au courant de l'irritant undefined is not a function, mais qu'en est-il de ceci :
// Déclare une classe qui _extends_ `null`
class Foo extends null {}
// -> [Function: Foo]
new Foo() instanceof null;
// > TypeError: function is not a function
// > at … … …
Ceci ne fait pas partie de la spécification. C'est seulement une erreur qui a depuis été corrigé, il ne devrait donc plus y avoir de problème à l'avenir.
Et si vous essayiez d'additionner deux tableaux ?
[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'
C'est la concaténation ! Etape par étape, ça ressemble à ceci :
[1, 2, 3] +
[4, 5, 6][
// appelle toString()
(1, 2, 3)
].toString() +
[4, 5, 6].toString();
// concaténation
"1,2,3" + "4,5,6";
// ->
("1,2,34,5,6");
Vous avez créé un tableau avec 4 éléments vides. Malgré tout, vous obtiendrez un tableau avec seulement trois éléments, à cause des virgules finales.
let a = [, , ,];
a.length; // -> 3
a.toString(); // -> ',,'
Les virgules finales (trailing commas en anglais) peuvent être utiles lors de l'ajout de nouveaux éléments, de paramètres ou de propriétés à du code JavaScript. Si vous voulez ajouter une nouvelle propriété, vous pouvez tout simplement ajouter une nouvelle ligne sans modifier la ligne précédente si cette ligne utilise déjà une virgule finale. Cela rend plus clair les différences dans un système de contrôle de version et l'édition de code pourrait être moins difficile.
— Virgules finales (trailing commas) sur MDN.
En JavaScript, l'égalité des tableaux est un monstre, comme vous pouvez le voir ci-dessous :
[] == '' // -> true
[] == 0 // -> true
[''] == '' // -> true
[0] == 0 // -> true
[0] == '' // -> false
[''] == 0 // -> true
[null] == '' // true
[null] == 0 // true
[undefined] == '' // true
[undefined] == 0 // true
[[]] == 0 // true
[[]] == '' // true
[[[[[[]]]]]] == '' // true
[[[[[[]]]]]] == 0 // true
[[[[[[ null ]]]]]] == 0 // true
[[[[[[ null ]]]]]] == '' // true
[[[[[[ undefined ]]]]]] == 0 // true
[[[[[[ undefined ]]]]]] == '' // true
Vous devriez regarder très attentivement les exemples ci-dessus ! Ce comportement est décrit dans la section 7.2.13 Abstract Equality Comparison de la spécification.
Si nous ne transmettons aucun argument dans le constructeur Number
, nous obtiendrons 0
. La valeur undefined
est attribuée aux arguments formels en l'absence d'arguments, alors vous pouvez vous attendre à ce que Number
sans argument utilise undefined
comme valeur de paramètre. Toutefois, quand nous lui passons undefined
directement, nous obtiendrons NaN
en retour.
Number(); // -> 0
Number(undefined); // -> NaN
Selon la spécification :
- Si aucun argument n'a été passé lors de l'appel de la fonction,
n
est+0
. - Sinon,
n
est ?ToNumber(value)
. - Dans le cas d'
undefined
,ToNumber(undefined)
doit retournerNaN
.
Voici les sections correspondantes :
parseInt
est célèbre pour ses bizarreries:
parseInt("f*ck"); // -> NaN
parseInt("f*ck", 16); // -> 15
💡 Explication : Ce résultat est dû au fait que parseInt
continue d'analyser caractère par caractère la chaîne jusqu'à ce qu'il rencontre un caractère qu'il ne connaît pas. Le f
dans 'f*ck'
correspond au chiffre hexadécimal 15
.
Analyser Infinity
comme entier est quelque chose…
//
parseInt("Infinity", 10); // -> NaN
// ...
parseInt("Infinity", 18); // -> NaN...
parseInt("Infinity", 19); // -> 18
// ...
parseInt("Infinity", 23); // -> 18...
parseInt("Infinity", 24); // -> 151176378
// ...
parseInt("Infinity", 29); // -> 385849803
parseInt("Infinity", 30); // -> 13693557269
// ...
parseInt("Infinity", 34); // -> 28872273981
parseInt("Infinity", 35); // -> 1201203301724
parseInt("Infinity", 36); // -> 1461559270678...
parseInt("Infinity", 37); // -> NaN
Soyez prudent avec l'analyse de null
aussi :
parseInt(null, 24); // -> 23
💡 Explication :
parsetInt
convertitnull
sous forme de chaîne"null"
et essait de la convertir. Pour les bases comprises entre 0 et 23,parseInt
ne peut convertir aucun chiffre, doncparseInt
renvoieNaN
. Sur 24,"n"
, la 14ème lettre, est ajoutée au système de numération. Sur 31,"u"
, la 21ème lettre, est ajoutée et la chaîne entière peut être décodée. Sur 37, il n'y a plus de jeu de valeur numérique valide pouvant être générée, donc,NaN
est renvoyé.— “parseInt(null, 24) === 23… wait, what?” sur StackOverflow.
N'oubliez pas les octaux:
parseInt("06"); // 6
parseInt("08"); // 8 si support ECMAScript 5
parseInt("08"); // 0 si pas support ECMAScript 5
💡 Explication : Si la chaîne d'entrée commence par 0, la base est égale à 8 (octal) ou à 10 (décimal). La base choisie dépend de l'implémentation. ECMAScript 5 indique que la valeur 10 (décimal) est utilisée, mais tous les navigateurs ne le prennent pas encore en charge. Pour cette raison, spécifiez toujours une base lorsque vous utilisez parseInt
.
parseInt
convertit toujours une entrée en chaîne :
parseInt({ toString: () => 2, valueOf: () => 1 }); // -> 2
Number({ toString: () => 2, valueOf: () => 1 }); // -> 1
Soyez prudent lors d'analyse de valeurs en virgule flottante :
parseInt(0.000001); // -> 0
parseInt(0.0000001); // -> 1
parseInt(1 / 1999999); // -> 5
💡 Explication : parseInt
prend une chaîne de caractère comme argument et retourne un entier sur la base spécifiée. parseInt
supprime aussi tout ce que suit, incluant le premier non-chiffre de la chaîne transmise en paramètre. 0.000001
est converti en chaîne "0.000001"
, et parseInt
retourne 0
. Quand 0.0000001
est converti en chaîne, il est traité comme "1e-7"
et retourne donc 1
. 1/1999999
est interprété comme étant 5.00000250000125e-7
et retourne 5
.
Faisons des maths :
true +
true(
// -> 2
true + true
) *
(true + true) -
true; // -> 3
Hmmm… 🤔
Avec le constructeur Number
, nous pouvons forcer les valeurs aux nombres. Il est assez évident que true
sera forcé à 1
.
Number(true); // -> 1
L'opérateur unaire +
tente de convertir sa valeur en nombre. Il peut convertir des représentations d'entiers et de nombres flottants sous forme de chaîne, de même que les valeurs true
, false
et null
. S'il ne peut pas analyser une valeur particulière, il sera évalué à NaN
. Cela signifie que nous pouvons contraindre true
à 1
plus facilement :
+true; // -> 1
Lorsque vous effectuez une addition ou une multiplication, la méthode ToNumber
est appelée. Selon la spécification, cette méthode retourne :
Si
argument
est true, retourne 1. Siargument
est false, retourne +0.
C'est pourquoi nous pouvons ajouter des valeurs booléennes en tant que nombres réguliers et obtenir des résultats corrects.
Les sections correspondantes :
Vous serez impressionné, mais <!--
(connu sous le nom de commentaire HTML) est aussi valide comme commentaire en JavaScript.
// commentaire valide
<!-- commentaire valide aussi
Impressionné ? Les commentaires de type HTML étaient à la base destinés à permettre aux navigateurs ne comprenant pas la balise <script>
de se dégrader avec élégance. Ces navigateurs, par exemple Netscape 1.x, ne sont plus populaires aujourd'hui. Il est donc inutile de mettre des commenaitres HTML dans vos balises <script>
.
typeof
NaN
est un 'number'
:
typeof NaN; // -> 'number'
Explications sur le fonctionnement des opérateurs typeof
et instanceof
:
typeof []; // -> "object"
typeof null; // -> "object"
// however
null instanceof Object; // false
Le comportement de l'opérateur typeof
est défini dans la section suivante de la spécification :
Selon la spécification, l'opérateur typeof
renvoie une chaîne : Table 35: typeof
Operator Results. Pour les objets null
, ordinaires, standard exotic et non-standard exotic, qui n'implémentent pas [[Call]]
, il retourne la chaîne "object"
.
En revanche, vous pouvez vérifier le type d'un objet en utilisant la méthode toString
.
Object.prototype.toString.call([]);
// -> '[object Array]'
Object.prototype.toString.call(new Date());
// -> '[object Date]'
Object.prototype.toString.call(null);
// -> '[object Null]'
999999999999999; // -> 999999999999999
9999999999999999; // -> 10000000000000000
10000000000000000; // -> 10000000000000000
10000000000000000 + 1; // -> 10000000000000000
10000000000000000 + 1.1; // -> 10000000000000002
Ceci est dû à la norme IEEE 754-2008 concernant l'arithmétique binaire en virgule flottante. À cette échelle, un nombre s'arrondit au nombre pair le plus près. Plus d'infos :
- 6.1.6 The Number Type
- IEEE 754 sur Wikipedia
Une blague bien connue. L'ajout de 0.1
et de 0.2
est mortellement précis :
0.1 +
0.2(
// -> 0.30000000000000004
0.1 + 0.2
) ===
0.3; // -> false
La réponse à la question ”Is floating point math broken?” sur StackOverflow:
Les constantes
0.2
et0.3
seront également des approximations à leurs vraies valeurs. Il arrive que ledouble
le plus proche de0.2
soit supérieur au nombre rationnel0.2
, mais que ledouble
le plus proche de0.3
soit inférieur au nombre rationnel0.3
. La somme de0.1
et0.2
finit donc par être supérieure au nombre rationnel0.3
et donc, en désaccord avec la constante de votre code.
Le problème est tellement connu qu'il y a même un site web appelé 0.30000000000000004.com. Il est récurrent pour tous les langages utilisant des mathématiques avec des virgules flottantes, pas seulement JavaScript.
Vous pouvez ajouter vos propres méthodes pour encapsuler des objets comme Number
ou String
.
Number.prototype.isOne = function() {
return Number(this) === 1;
};
(1.0).isOne(); // -> true
(1).isOne(); // -> true
(2.0)
.isOne()(
// -> false
7
)
.isOne(); // -> false
De toute évidence, vous pouvez extend l'objet Number
comme n'importe quel autre objet en JavaScript, mais ce n'est toutefois pas recommandé si le comportement de la méthode définie ne fait partie de la spécification. Voici donc la liste des propriétés de Number
:
1 < 2 < 3; // -> true
3 > 2 > 1; // -> false
Pourquoi est-ce que cela fonctionne ainsi ? Et bien le problème se trouve dans la première partie de l'expression. Voici comment cela fonctionne :
1 < 2 < 3; // 1 < 2 -> true
true < 3; // true -> 1
1 < 3; // -> true
3 > 2 > 1; // 3 > 2 -> true
true > 1; // true -> 1
1 > 1; // -> false
Nous pouvons résoudre ce problème avec l'opérateur Supérieur ou égal à (>=
) :
3 > 2 >= 1; // true
En savoir plus sur les opérateurs relationnels dans la spécification :
Souvent, les résultats d'opérations arithmétiques en JavaScript peuvent être assez inattendus. Considérez ces exemples :
3 - 1 // -> 2
3 + 1 // -> 4
'3' - 1 // -> 2
'3' + 1 // -> '31'
'' + '' // -> ''
[] + [] // -> ''
{} + [] // -> 0
[] + {} // -> '[object Object]'
{} + {} // -> '[object Object][object Object]'
'222' - -'111' // -> 333
[4] * [4] // -> 16
[] * [] // -> 0
[4, 4] * [4, 4] // NaN
Que se passe-t-il dans les quatre premiers exemples ? Voici un petit tableau pour comprendre les additions en JavaScript :
Nombre + Nombre -> addition
Booléen + Nombre -> addition
Booléen + Booléen -> addition
Nombre + Chaîne -> concaténation
Chaîne + Booléen -> concaténation
Chaîne + Chaîne -> concaténation
Qu'en est-il des autres exemples ? Les méthodes ToPrimitive
et ToString
sont implicitement appelées pour []
et {}
avant une addition. En lire plus sur le processus d'évalution dans la spécification :
- 12.8.3 The Addition Operator (
+
) - 7.1.1 ToPrimitive(
input
[,PreferredType
]) - 7.1.12 ToString(
argument
)
Saviez-vous que vous pouviez ajouter des nombres comme dans l'exemple ci-dessous ?
// Remplacement de la méthode toString
RegExp.prototype.toString =
function() {
return this.source;
} /
7 /
-/5/; // -> 2
"str"; // -> 'str'
typeof "str"; // -> 'string'
"str" instanceof String; // -> false
Le constructeur String
retourne une chaîne :
typeof String("str"); // -> 'string'
String("str"); // -> 'str'
String("str") == "str"; // -> true
Essayons avec un new
:
new String("str") == "str"; // -> true
typeof new String("str"); // -> "object"
Un objet ? Qu'est-ce que c'est ?
new String("str"); // -> [String: 'str']
Plus d'infos sur le constructeur String
dans la spécification :
Déclarons une fonction qui enregistre tous les paramètres dans la console :
function f(...args) {
return args;
}
Aucun doute, vous savez qu'il est possible d'appeler cette fonction comme ceci :
f(1, 2, 3); // -> [ 1, 2, 3 ]
Mais saviez-vous que vous pouvez appeler n'importe quelle fonction avec des caractères accent grave (backticks en anglais) ?
f`true is ${true}, false is ${false}, array is ${[1, 2, 3]}`;
// -> [ [ 'true is ', ', false is ', ', array is ', '' ],
// -> true,
// -> false,
// -> [ 1, 2, 3 ] ]
Bon, ce n'est pas du tout magique si vous êtes familier des littéraux de gabarits étiquetés. Dans l'exemple ci-dessus, la fonction f
est une étiquette pour littéral de gabarit. Les étiquettes avant un littéral de gabarit vous permettent d'analyser les littéraux de gabarits avec une fonction. Le premier argument d'une fonction étiquetée contient un tableau avec comme valeurs des chaînes. Les arguments restants sont liés aux expressions. Exemple :
function template(strings, ...keys) {
// fait quelque chose avec `strings` et `keys`…
}
Voici la magic behind la célébre bibliothèque appelée 💅 styled-components, qui est populaire dans la communauté React.
Lien vers la spécification :
Trouvé par @cramforce
console.log.call.call.call.call.call.apply(a => a, [1, 2]);
Attention, ceci pourrait vous casser la tête ! Essayez de reproduire ce code dans votre tête : nous appliquons la méthode call
en utilisant la méthode apply
. Plus d'infos :
- 19.2.3.3 Function.prototype.call(
thisArg
, ...args
) - **19.2.3.1 ** Function.prototype.apply(
thisArg
,argArray
)
const c = "constructor";
c[c][c]('console.log("WTF?")')(); // > WTF?
Considérez cet exemple étape par étape :
// Déclare une nouvelle constante qui est une chaîne : "constructor"
const c = "constructor";
// `c` est une chaîne
c; // -> 'constructor'
// Pour obtenir le constructeur de la chaîne
c[c]; // -> [Function: String]
// Pour obtenir le constructeur du constructeur
c[c][c]; // -> [Function: Function]
// Appelle la Fonction constructeur et passe
// le corps d'une nouvelle fonction comme argument
c[c][c]('console.log("WTF?")'); // -> [Function: anonymous]
// Et ensuite appelle cette fonction anonyme
// Le résultat est un retour de console avec la chaîne "WTF?"
c[c][c]('console.log("WTF?")')(); // > "WTF?"
Un Object.prototype.constructor
renvoie une référence à la fonction constructeur Object
qui a créé l'instance de l'objet. Dans le cas des chaînes, il s'agit de String
, dans le cas des nombres, il s'agit de Number
, et ainsi de suite.
{ [{}]: {} } // -> { '[object Object]': {} }
Pourquoi est-ce que ça marche ? Ici, nous utilisons un Computed property name. Quand vous passez un objet entre ces crochets, l'objet est forcé de devenir une chaîne, alors nous obtenons la clé [objet Object]
et la valeur {}
.
Nous pouvons créer des enfers de crochets et de parenthèses comme dans l'exemple ci-dessous :
({ [{}]: { [{}]: {} } }[{}][{}]); // -> {}
// structure:
// {
// '[object Object]': {
// '[object Object]': {}
// }
// }
En savoir plus sur les objets littéraux ici:
Comme nous le savons, les primitives n’ont pas de prototypes. Cependant, si nous essayons d'obtenir une valeur de __proto__
pour les primitives, nous obtiendrions ceci :
(1).__proto__.__proto__.__proto__; // -> null
Cela se produit car lorsque quelque chose n'a pas de prototype, il sera encapsulé dans un objet à l'aide de la méthode ToObject
. Donc, étape par étape :
(1)
.__proto__(
// -> [Number: 0]
1
)
.__proto__.__proto__(
// -> {}
1
).__proto__.__proto__.__proto__; // -> null
Voici plus d'infos sur __proto__
:
Quel est le résultat de l'expression ci-dessous ?
`${{ Object }}`;
La réponse est :
// -> '[object Object]'
Nous avons défini un objet avec une propriété Object
en utilisant une propriété de notation raccourcie :
{
Object: Object;
}
Ensuite, nous avons passé cet objet au litéral de gabarit, ce qui fait que la méthode toString
appelle donc cet objet. Voilà pourquoi nous obtenons la chaîne '[object Object]'
.
Considérez cet exemple :
let x,
{ x: y = 1 } = { x };
y;
L'exemple ci-dessus est une bonne question pour un entretien. Quelle est la valeur de y
? La réponse est :
// -> 1
let x,
{ x: y = 1 } = { x };
y;
// ↑ ↑ ↑ ↑
// 1 3 2 4
Avec l'exemple ci-dessus :
- On déclare
x
sans valeur, donc sa valeur estundefined
. - Ensuite, nous intégrons la valeur de
x
dans la propriété de l'objetx
. - Ensuite, nous extrayons la valeur de
x
en utilisant la déstructuration pour l'assigner ày
. Si la valeur n'est pas déinie, nous utiliserons1
comme valeur par défaut. - Retourne la valeur de
y
.
- Initialisateur d'objet sur MDN.
Des exemples intéressants pourraient être composés avec la propagation de tableaux. Considérez cela :
[...[..."..."]].length; // -> 3
Pourquoi 3
? Lorsque nous utilisons le spread operator, la méthode @@iterator
est appelée et l'itérateur renvoyé est utilisé pour obtenir les valeurs à itérer. L'itérateur par défaut pour une chaîne étend une chaîne en caractères. Après sa propagation, ces caractères sont empaquetés dans un tableau. Ensuite, ce tableau est à nouveau propagé et empaqueté encore une fois dans un tableau.
Une chaîne '...'
est composée de trois caractères .
, donc, la longueur du tableau résultant est de 3
.
Maintenant, étape par étape :
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...'...']].length // -> 3
Évidemment, nous pouvons étendre et encapsuler les éléments d’un tableau autant de fois que nous le souhaitons :
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...[...'...']]] // -> [ '.', '.', '.' ]
[...[...[...[...'...']]]] // -> [ '.', '.', '.' ]
// etc.
Peu de programmeurs connaissent les étiquettes en JavaScript. Elles sont plutôt intéressantes :
foo: {
console.log("first");
break foo;
console.log("second");
}
// -> first
// -> undefined
La déclaration étiquetée est utilisée avec les déclarations break
ou continue
. Vous pouvez utiliser une étiquette pour identifier une boucle, puis ensuite, utiliser la déclaration break
ou continue
pour indiquer si un programme doit interrompre la boucle ou poursuivre son exécution.
Dans l'exemple ci-dessus, nous identifions une étiquette foo
. Ensuite, console.log('first');
est exécuté et l'exécution est interrompue.
En savoir plus sur les étiquettes en JavaScript :
- 13.13 Labelled Statements
- Label sur MDN.
a: b: c: d: e: f: g: 1, 2, 3, 4, 5; // -> 5
Similaire aux exemples précédents, suivez ces liens pour plus d'infos :
Que retournera cette expression ? 2
ou 3
?
(() => {
try {
return 2;
} finally {
return 3;
}
})();
La réponse est 3
. Surprenant ?
Regardez l'exemple ci-dessous :
new class F extends (String, Array) {}(); // -> F []
Est-ce un héritage multiple ? Non.
L'élément intéressant est la valeur de la clause extends
((String, Array)
). L'opérateur de groupement retourne toujours son dernier argument, donc (String, Array)
est en réalité simplement Array
. Cela signifie que nous venons de créer une classe qui extends Array
.
Considérez cet exemple de générateur qui se yield
lui-même :
(function* f() {
yield f;
})().next();
// -> { value: [GeneratorFunction: f], done: false }
Comme vous pouvez le constater, la valeur renvoyée est un objet dont la valeur
est égale à f
. Dans ce cas, nous pouvons faire quelque chose semblable à ça :
(function* f() {
yield f;
})()
.next()
.value()
.next()(
// -> { value: [GeneratorFunction: f], done: false }
// and again
function* f() {
yield f;
}
)()
.next()
.value()
.next()
.value()
.next()(
// -> { value: [GeneratorFunction: f], done: false }
// and again
function* f() {
yield f;
}
)()
.next()
.value()
.next()
.value()
.next()
.value()
.next();
// -> { value: [GeneratorFunction: f], done: false }
// and so on
// …
Pour comprendre pourquoi cela fonctionne ainsi, lisez ces sections de la spécification :
Considérez cette syntaxe obfusquée :
typeof new class {
class() {}
}(); // -> "object"
Il semblerait que nous déclarions une classe à l'intérieur d'une classe. Cela devrait être une erreur, cependant, nous obtenons la chaîne "object"
.
Depuis ECMAScript 5, les mots clés sont autorisés en tant que noms de propriétés. Réfléchissez comment vous le feriez pour cet exemple d'objet simple :
const foo = {
class: function() {}
};
Et les définitions de méthode abrégée standard d'ES6. Aussi, les classes peuvent être anonymes. Donc, si nous supprimons la partie : function
, nous obtiendrons :
class {
class() {}
}
Le résultat d'une classe par défaut est toujours un objet simple. Et son typeof
devrait retourner "object"
.
Plus d'infos ici :
Avec des symboles bien connus, il existe un moyen de se débarrasser de la coercition de type. Examinez l'exemple ci-dessous :
function nonCoercible(val) {
if (val == null) {
throw TypeError(
"inCoercible ne doit pas être appelé avec null ou undefined"
);
}
const res = Object(val);
res[Symbol.toPrimitive] = () => {
throw TypeError("Tente de transformer un objet incoercible");
};
return res;
}
Maintenant, nous pouvons l'utiliser de cette manière :
// objets
const foo = nonCoercible({ foo: "foo" });
foo * 10; // -> TypeError: Tente de transformer un objet incoercible
foo + "evil"; // -> TypeError: Tente de transformer un objet incoercible
// chaînes
const bar = nonCoercible("bar");
bar + "1"; // -> TypeError: Tente de transformer un objet incoercible
bar.toString() + 1; // -> bar1
bar === "bar"; // -> false
bar.toString() === "bar"; // -> true
bar == "bar"; // -> TypeError: Tente de transformer un objet incoercible
// nombres
const baz = nonCoercible(1);
baz == 1; // -> Tente de transformer un objet incoercible
baz === 1; // -> false
baz.valueOf() === 1; // -> true
Considérez l'exemple ci-dessous :
let f = () => 10;
f(); // -> 10
D'accord, d'accord, mais qu'en est-il de celui-ci :
let f = () => {};
f(); // -> undefined
Vous pourriez vous attendre à obtenir {}
au lieu d'undefined
. C'est dû au fait que les accolades font partie de la syntaxe des fonctions fléchées; f
renverra donc undefined
. Il est cependant possible de renvoyer l'objet {}
directement à partir d'une fonction fléchée en mettant la valeur de retour entre parenthèses :
let f = () => ({});
f(); // -> {}
Considérez l'exemple ci-dessous :
let f = function() {
this.a = 1;
};
new f(); // -> { 'a': 1 }
Maintenant, essayez de faire la même chose avec une fonction fléchée :
let f = () => {
this.a = 1;
};
new f(); // -> TypeError: f is not a constructor
Les fonctions fléchées ne peuvent pas être utilisées en tant que constructeurs et produiront une erreur si elles sont utilisées avec new
parce qu'elles ont un lexical this
et pas de propriété prototype
, cela n'aurait donc pas beaucoup de sens.
Considérez l'exemple ci-dessous :
let f = function() {
return arguments;
};
f("a"); // -> { '0': 'a' }
Maintenant, essayez de faire la même chose avec une fonction fléchée :
let f = () => arguments;
f("a"); // -> Uncaught ReferenceError: arguments is not defined
Les fonctions fléchées sont une version allégée des fonctions standards dans laquelle l'accent est mis sur la taille et le lexical this
. En même temps, les fonctions fléchées ne fournissent pas de liaison pour l'objet arguments
. Comme alternative valable, utilisez les paramètres rest
pour obtenir le même résultat :
let f = (...args) => args;
f("a");
- Fonctions fléchées sur MDN.
La déclaration return
est compliquée aussi. Considérez ceci :
(function() {
return;
{
b: 10;
}
})(); // -> undefined
return
et l'expression renvoyée doivent être sur la même ligne :
(function() {
return {
b: 10
};
})(); // -> { b: 10 }
Cela est dû au concept appelé "Insertion Automatique du Point-Virgule", qui insère automatiquement des points-virgules à la fin de la plupart des nouvelles lignes. Dans le premier exemple, un point-virgule est inséré entre return
et l'objet littéral. La fonction renvoie donc undefined
et l'objet littéral n'est jamais évalué.
var foo = { n: 1 };
var bar = foo;
foo.x = foo = { n: 2 };
foo.x; // -> undefined
foo; // -> {n: 2}
bar; // -> {n: 1, x: {n: 2}}
De droite à gauche, {n: 2}
est affecté à foo
, et le résultat de cette affectation {n: 2}
est affecté à foo.x
. C'est pourquoi bar
retourne {n: 1, x: {n: 2}}
, puisque bar
est une référence à foo
. Mais pourquoi foo.x
retourne undefined
, alors que ce n'est pas le cas pour bar.x
?
foo
et bar
font référence au même objet {n: 1}
, et les lvalues sont résolues avant les assignations. foo = {n: 2}
crée un nouvel objet, et donc foo
est mis à jour pour référencer ce nouvel objet. L'astuce ici est foo
dans foo.x = …
, car une lvalue a été résolue au préalable et fait toujours référence à l'objet précédent foo = {n: 1}
et donc le met à jour en ajoutant la valeur x
.
Après cette chaîne d'assignation, bar
, quant à lui, fait toujours référence à l'ancien objet foo
, alors que foo
fait référence au nouvel objet {n: 2}
, où x
n'existe pas.
C'est équivalent à :
var foo = { n: 1 };
var bar = foo;
foo = { n: 2 }; // -> {n: 2}
bar.x = foo; // -> {n: 1, x: {n: 2}}
// `bar.x` pointe sur l'adresse du nouvel objet `foo`
// ce n'est pas équivalent à : `bar.x = {n: 2}`
var obj = { property: 1 };
var array = ["property"];
obj[array]; // -> 1
Qu'en est-il des tableaux pseudo-multidimensionnels ?
var map = {};
var x = 1;
var y = 2;
var z = 3;
map[[x, y, z]] = true;
map[[x + 10, y, z]] = true;
map["1,2,3"]; // -> true
map["11,2,3"]; // -> true
L'opérateur []
convertit l'expression passée en utilisant toString
. Transformer un tableau composé d'un seul élément vers une chaîne est similaire à transformer l'élément du tableau en chaîne :
["property"].toString(); // -> 'property'
null > 0; // false
null == 0; // false
null >= 0; // true
En bref, si null < 0
est false
, alors null >= 0
est true
. Lisez une explication détaillée ici.
Number.toFixed()
peut se comporter étrangement selon le navigateur utilisé. Voir cet exemple :
(0.7875).toFixed(3);
// Firefox: -> 0.787
// Chrome: -> 0.787
// IE11: -> 0.788
(0.7876).toFixed(3);
// Firefox: -> 0.788
// Chrome: -> 0.788
// IE11: -> 0.788
Alors que votre premier instinct peut être que IE11 a correct et que Firefox et Chrome ont faux, la réalité est que Firefox et Chrome obéissent plus directement aux normes relatives aux nombres (IEEE-754 Floating Point), alors qu'IE11 les désobéit minutieusement dans (ce qui est probablement) un effort de donner des résultats plus clairs.
Vous pouvez voir pourquoi cela se produit avec quelques tests rapides :
// Confirme le résultat curieux d'arrondir `5` vers le bas
(0.7875).toFixed(3); // -> 0.787
// Il semble que ce soit juste `5` lorsque vous développez
// les limites de la précision de flottement de 64 bits (double précision)
(0.7875).toFixed(14); // -> 0.78750000000000
// Mais que se passe-t-il si vous allez au-delà de la limite ?
(0.7875).toFixed(20); // -> 0.78749999999999997780
Les nombres à virgule flottante ne sont pas stockés sous forme de liste de chiffres décimaux en interne, mais par le biais d'une méthodologie plus complexe qui produit de minuscules inexactitudes qui sont généralement arrondies par des appels à toString
ou des appels similaires, mais qui sont réellement présentes en interne.
Dans ce cas, le 5
sur la fin était en réalité une fraction extrêmement petite, en dessous d'un vrai 5
. Si vous arrondissez à une longueur raisonnable, vous obtenez un 5
… mais ce n'est en réalité pas un 5
en interne.
Ceci étant dit, IE11 rapportera la valeur entrée uniquement avec des zéros ajoutés à la fin, même dans le cas de toFixed(20)
, car cela semble forcer la valeur arrondie pour réduire les problèmes liées aux limites matérielles.
Voir pour référence NOTE 2
sur la définition de ECMA-262 pour toFixed
.
Math.min(1, 4, 7, 2); // -> 1
Math.max(1, 4, 7, 2); // -> 7
Math.min(); // -> Infinity
Math.max(); // -> -Infinity
Math.min() > Math.max(); // -> true
- Why is Math.max() less than Math.min()? par Charlie Harvey.
Les expressions suivantes semblent introduire une contradiction :
null == 0; // -> false
null > 0; // -> false
null >= 0; // -> true
Comment null
peut-il être ni égal ni supérieur à 0
, si null >= 0
est en fait true
? (Ceci fonctionne de la même manière avec inférieur ou égal à (<=
).)
Les méthodes d'évaluation de ces trois expressions sont toutes différentes et sont responsables de la production de ce comportement inattendu.
Premièrement, la comparaison d'égalité abstraite null == 0
. Normalement, si cet opérateur ne peut pas comparer correctement les valeurs d'un côté comme de l'autre, il convertit les deux en nombres et compare ensuite les nombres. Alors, vous pouvez vous attendre au comportement suivant :
// Ce n'est pas ce qui se passe
(null == 0 + null) == +0;
0 == 0;
true;
Cependant, suite à une lecture attentive de la spécification, la transformation du nombre n'a pas lieu sur un côté qui est null
ou undefined
. Par conséquent, si vous avez null
sur un des deux côté du signe égal, l'autre côté doit être null
ou undefined
pour que l'expression retourne true
. Comme ce n'est pas le cas, false
est renvoyé.
Ensuite, la comparaison relationnelle null > 0
. L'algorithme ici, contrairement à celui de l'opérateur d'égalité abstraite, va convertir null
à un nombre. Donc, nous obtenons ce comportement :
null > 0
+null = +0
0 > 0
false
Finalement, la comparaison relationnelle null >= 0
. Vous pourriez soutenir que cette expression devrait être le résultat de null > 0 || null == 0
; si tel était le cas, les résultats ci-dessus signifieraient que ceci serait aussi false
. Toutefois, l'opérateur >=
fonctionne d'une manière très différente, qui est simplement d'utiliser le contraire de l'opérateur <
. Parce que notre exemple ci-dessus, utilisant l'opérateur supérieur à, s'applique également à l'opérateur inférieur à, cela signifie que cette expression est réellement évaluée de la manière suivante :
null >= 0;
!(null < 0);
!(+null < +0);
!(0 < 0);
!false;
true;
JavaScript autorise la redéclaration de variables :
a;
a;
// Ceci est aussi valide
a, a;
Et ça fonctionne aussi en mode strict
:
var a, a, a;
var a;
var a;
Toutes les définitions fusionnent en une seule définition.
Imaginez que vous ayez besoin de trier un tableau composé de nombres.
[ 10, 1, 3 ].sort() // -> [ 1, 10, 3 ]
L'ordre de tri par défaut est construit lors de la transformation des éléments en chaînes, puis en comparant leurs séquences de valeurs unitaires sur le jeu de caractères UTF-16.
Passez comparefn
si vous essayez de trier n'importe quoi d'autre qu'une chaîne.
[ 10, 1, 3 ].sort((a, b) => a - b) // -> [ 1, 3, 10 ]
- wtfjs.com — une collection d'irrégularités spéciales très particulières, d'incohérences et de moments terriblement non intuitifs pour le langage du Web.
- Wat — Un discours éclair de Gary Bernhardt de CodeMash 2012.
- What the... JavaScript? — Kyle Simpsons parle des tentatives de Forward 2 pour "sortir de l'absurdité" du JavaScript. Il veut vous aider à produire un code plus propre, plus élégant et plus lisible, puis inspirer les gens à contribuer à la communauté open-source.