Explication de la contrainte de type JavaScript

Connaissez vos moteurs

[Edit 2/5/2018] : Ce message est maintenant disponible en russe. Applaudissements à Serj Bulavyk pour ses efforts.

La coercition de type est le processus de conversion d'une valeur d'un type à un autre (comme une chaîne en nombre, un objet en booléen, etc.). Tout type, qu'il soit primitif ou objet, est un sujet valide pour la coercition de type. Pour rappel, les primitives sont: nombre, chaîne, booléen, nul, non défini + symbole (ajouté dans ES6).

À titre d'exemple de coercition de type dans la pratique, regardez le tableau de comparaison JavaScript, qui montre comment l' ==opérateur d' égalité lâche se comporte pour différents types aet b. Cette matrice semble effrayante en raison de la coercition de type implicite de l' ==opérateur, et il est difficile de se souvenir de toutes ces combinaisons. Et vous n'êtes pas obligé de faire cela - apprenez simplement les principes de coercition de type sous-jacents.

Cet article explique en détail le fonctionnement de la coercition de type en JavaScript et vous fournira les connaissances essentielles, afin que vous puissiez être sûr d'expliquer à quoi servent les expressions suivantes. À la fin de l'article, je vais montrer les réponses et les expliquer.

true + false 12 / "6" "number" + 15 + 3 15 + 3 + "number" [1] > null "foo" + + "bar" 'true' == true false == 'false' null == '' !!"false" == !!"true" [‘x’] == ‘x’ [] + null + 1 [1,2,3] == [1,2,3] {}+[]+{}+[1] !+[]+[]+![] new Date(0) - 0 new Date(0) + 0

Oui, cette liste regorge de choses assez stupides que vous pouvez faire en tant que développeur. Dans 90% des cas d'utilisation, il vaut mieux éviter la contrainte de type implicite. Considérez cette liste comme un exercice d'apprentissage pour tester vos connaissances sur le fonctionnement de la coercition de type. Si vous vous ennuyez, vous pouvez trouver plus d'exemples sur wtfjs.com.

En passant, vous pourriez parfois rencontrer de telles questions lors de l'entretien pour un poste de développeur JavaScript. Alors, continuez à lire?

Coercition implicite ou explicite

La coercition de type peut être explicite et implicite.

Lorsqu'un développeur exprime l'intention de convertir entre les types en écrivant le code approprié, comme Number(value), cela s'appelle la coercition de type explicite (ou conversion de type).

Étant donné que JavaScript est un langage faiblement typé, les valeurs peuvent également être converties automatiquement entre différents types, et cela s'appelle la coercition de type implicite . Cela se produit généralement lorsque vous appliquez des opérateurs à des valeurs de types différents, comme

1 == null, 2/’5', null + new Date()Ou il peut être déclenché par le contexte environnant, comme avec if (value) {…}, où valueest sous la contrainte à booléen.

Un opérateur qui ne déclenche pas la coercition de type implicite est ===, qui est appelé l'opérateur d'égalité stricte. L'opérateur d'égalité lâche, ==d'autre part, effectue à la fois la comparaison et la coercition de type si nécessaire.

La coercition de type implicite est une épée à double tranchant: c'est une grande source de frustration et de défauts, mais aussi un mécanisme utile qui nous permet d'écrire moins de code sans perdre la lisibilité.

Trois types de conversion

La première règle à savoir est qu'il n'y a que trois types de conversion en JavaScript:

  • enchaîner
  • en booléen
  • numéroter

Deuxièmement, la logique de conversion pour les primitives et les objets fonctionne différemment, mais les primitives et les objets ne peuvent être convertis que de ces trois manières.

Commençons par les primitives en premier.

Conversion de chaîne

Pour convertir explicitement des valeurs en chaîne, appliquez la String()fonction. La coercition implicite est déclenchée par l' +opérateur binaire , lorsqu'un opérande est une chaîne:

String(123) // explicit 123 + '' // implicit

Toutes les valeurs primitives sont converties en chaînes naturellement comme vous pouvez vous y attendre:

String(123) // '123' String(-12.3) // '-12.3' String(null) // 'null' String(undefined) // 'undefined' String(true) // 'true' String(false) // 'false'

La conversion de symboles est un peu délicate, car elle ne peut être convertie que explicitement, mais pas implicitement. En savoir plus sur les Symbolrègles de coercition.

String(Symbol('my symbol')) // 'Symbol(my symbol)' '' + Symbol('my symbol') // TypeError is thrown

Conversion booléenne

Pour convertir explicitement une valeur en booléen, appliquez la Boolean()fonction.

La conversion implicite se produit dans un contexte logique ou est déclenchée par des opérateurs logiques ( ||&&!).

Boolean(2) // explicit if (2) { ... } // implicit due to logical context !!2 // implicit due to logical operator 2 || 'hello' // implicit due to logical operator

Remarque : Les opérateurs logiques tels que ||et &&effectuent des conversions booléennes en interne, mais renvoient en fait la valeur des opérandes d'origine, même s'ils ne sont pas booléens.

// returns number 123, instead of returning true // 'hello' and 123 are still coerced to boolean internally to calculate the expression let x = 'hello' && 123; // x === 123

Dès qu'il n'y a que 2 résultats possibles de la conversion booléenne: trueou false, il est simplement plus facile de se souvenir de la liste des valeurs fausses.

Boolean('') // false Boolean(0) // false Boolean(-0) // false Boolean(NaN) // false Boolean(null) // false Boolean(undefined) // false Boolean(false) // false

Toute valeur qui ne sont pas dans la liste est convertie true, y compris l' objet, la fonction Array, Datetype défini par l' utilisateur, et ainsi de suite. Les symboles sont des valeurs de vérité. Les objets vides et les tableaux sont également des valeurs de vérité:

Boolean({}) // true Boolean([]) // true Boolean(Symbol()) // true !!Symbol() // true Boolean(function() {}) // true

Conversion numérique

Pour une conversion explicite, appliquez simplement la Number()fonction, comme vous l'avez fait avec Boolean()et String().

La conversion implicite est délicate, car elle est déclenchée dans plus de cas:

  • des opérateurs de comparaison ( >, <, <=, >=)
  • opérateurs au niveau du bit ( |&^~)
  • opérateurs arithmétiques ( -+*/%). Notez que ce binaire +ne déclenche pas de conversion numérique, lorsqu'un opérande est une chaîne.
  • +opérateur unaire
  • opérateur d'égalité lâche ==(incl. !=).

    Notez que ==ne déclenche pas la conversion numérique lorsque les deux opérandes sont des chaînes.

Number('123') // explicit +'123' // implicit 123 != '456' // implicit 4 > '5' // implicit 5/null // implicit true | 0 // implicit

Voici comment les valeurs primitives sont converties en nombres:

Number(null) // 0 Number(undefined) // NaN Number(true) // 1 Number(false) // 0 Number(" 12 ") // 12 Number("-12.34") // -12.34 Number("\n") // 0 Number(" 12s ") // NaN Number(123) // 123

Lors de la conversion d' une chaîne en nombre, le moteur premier garnitures espaces avant et, \n, \tcaractères, retour NaNsi la chaîne ne représente pas coupé un nombre valide. Si la chaîne est vide, elle retourne 0.

nullet undefinedsont traités différemment: nulldevient 0, alors que undefineddevient NaN.

Les symboles ne peuvent pas être convertis en nombre ni explicitement ni implicitement. De plus, TypeErrorest jeté, au lieu de se convertir silencieusement en NaN, comme cela arrive pour undefined. En savoir plus sur les règles de conversion de symboles sur MDN.

Number(Symbol('my symbol')) // TypeError is thrown +Symbol('123') // TypeError is thrown

Il y a deux règles spéciales à retenir:

  1. Lors de l'application ==à nullou undefined, la conversion numérique ne se produit pas. nullégale seulement à nullou undefined, et n'égale à rien d'autre.
null == 0 // false, null is not converted to 0 null == null // true undefined == undefined // true null == undefined // true

2. NaN ne vaut rien, même lui-même:

if (value !== value) { console.log("we're dealing with NaN here") }

Coercition de type pour les objets

Jusqu'à présent, nous avons examiné la coercition de type pour les valeurs primitives. Ce n'est pas très excitant.

Lorsqu'il s'agit d'objets et que le moteur rencontre des expressions comme [1] + [2,3], il doit d'abord convertir un objet en une valeur primitive, qui est ensuite convertie en type final. Et il n'y a toujours que trois types de conversion: numérique, chaîne et booléenne.

Le cas le plus simple est la conversion booléenne: toute valeur non primitive est toujours

forcé à true, peu importe si un objet ou un tableau est vide ou non.

Les objets sont convertis en primitives via la [[ToPrimitive]]méthode interne , qui est responsable de la conversion numérique et chaîne.

Voici une pseudo implémentation de [[ToPrimitive]]méthode:

[[ToPrimitive]]est transmis avec une valeur d'entrée et un type de conversion préféré: Numberou String. preferredTypeest facultatif.

Both numeric and string conversion make use of two methods of the input object: valueOf and toString . Both methods are declared on Object.prototype and thus available for any derived types, such as Date, Array, etc.

In general the algorithm is as follows:

  1. If input is already a primitive, do nothing and return it.

2. Call input.toString(), if the result is primitive, return it.

3. Call input.valueOf(), if the result is primitive, return it.

4. If neither input.toString() nor input.valueOf() yields primitive, throw TypeError.

Numeric conversion first calls valueOf (3) with a fallback to toString (2). String conversion does the opposite: toString (2) followed by valueOf (3).

Most built-in types do not have valueOf, or have valueOf returning this object itself, so it’s ignored because it’s not a primitive. That’s why numeric and string conversion might work the same — both end up calling toString().

Different operators can trigger either numeric or string conversion with a help of preferredType parameter. But there are two exceptions: loose equality == and binary + operators trigger default conversion modes (preferredType is not specified, or equals to default). In this case, most built-in types assume numeric conversion as a default, except Date that does string conversion.

Here is an example of Date conversion behavior:

You can override the default toString() and valueOf() methods to hook into object-to-primitive conversion logic.

Notice how obj + ‘’ returns ‘101’ as a string. + operator triggers a default conversion mode, and as said before Object assumes numeric conversion as a default, thus using the valueOf() method first instead of toString().

ES6 Symbol.toPrimitive method

In ES5 you can hook into object-to-primitive conversion logic by overriding toString and valueOf methods.

In ES6 you can go farther and completely replace internal[[ToPrimitive]] routine by implementing the[Symbol.toPrimtive] method on an object.

Examples

Armed with the theory, now let’s get back to our examples:

true + false // 1 12 / "6" // 2 "number" + 15 + 3 // 'number153' 15 + 3 + "number" // '18number' [1] > null // true "foo" + + "bar" // 'fooNaN' 'true' == true // false false == 'false' // false null == '' // false !!"false" == !!"true" // true ['x'] == 'x' // true [] + null + 1 // 'null1' [1,2,3] == [1,2,3] // false {}+[]+{}+[1] // '0[object Object]1' !+[]+[]+![] // 'truefalse' new Date(0) - 0 // 0 new Date(0) + 0 // 'Thu Jan 01 1970 02:00:00(EET)0'

Below you can find explanation for each the expression.

Binary + operator triggers numeric conversion for true and false

true + false ==> 1 + 0 ==> 1

Arithmetic division operator / triggers numeric conversion for string '6' :

12 / '6' ==> 12 / 6 ==>> 2

Operator + has left-to-right associativity, so expression "number" + 15 runs first. Since one operand is a string, + operator triggers string conversion for the number 15. On the second step expression "number15" + 3 is evaluated similarly.

“number” + 15 + 3 ==> "number15" + 3 ==> "number153"

Expression 15 + 3 is evaluated first. No need for coercion at all, since both operands are numbers. On the second step, expression 18 + 'number' is evaluated, and since one operand is a string, it triggers a string conversion.

15 + 3 + "number" ==> 18 + "number" ==> "18number"

Comparison operator &gt; triggers numeric conversion for [1] and null .

[1] > null ==> '1' > 0 ==> 1 > 0 ==> true

Unary + operator has higher precedence over binary + operator. So +'bar' expression evaluates first. Unary plus triggers numeric conversion for string 'bar'. Since the string does not represent a valid number, the result is NaN. On the second step, expression 'foo' + NaN is evaluated.

"foo" + + "bar" ==> "foo" + (+"bar") ==> "foo" + NaN ==> "fooNaN"

== operator triggers numeric conversion, string 'true' is converted to NaN, boolean true is converted to 1.

'true' == true ==> NaN == 1 ==> false false == 'false' ==> 0 == NaN ==> false

== usually triggers numeric conversion, but it’s not the case with null . null equals to null or undefined only, and does not equal to anything else.

null == '' ==> false

!! operator converts both 'true' and 'false' strings to boolean true, since they are non-empty strings. Then, == just checks equality of two boolean true's without any coercion.

!!"false" == !!"true" ==> true == true ==> true

== operator triggers a numeric conversion for an array. Array’s valueOf() method returns the array itself, and is ignored because it’s not a primitive. Array’s toString() converts ['x'] to just 'x' string.

['x'] == 'x' ==> 'x' == 'x' ==> true

+ operator triggers numeric conversion for []. Array’s valueOf() method is ignored, because it returns array itself, which is non-primitive. Array’s toString returns an empty string.

On the the second step expression '' + null + 1 is evaluated.

[] + null + 1 ==> '' + null + 1 ==> 'null' + 1 ==> 'null1'

Logical || and && operators coerce operands to boolean, but return original operands (not booleans). 0 is falsy, whereas '0' is truthy, because it’s a non-empty string. {} empty object is truthy as well.

0 || "0" && {} ==> (0 || "0") && {} ==> (false || true) && true // internally ==> "0" && {} ==> true && true // internally ==> {}

No coercion is needed because both operands have same type. Since == checks for object identity (and not for object equality) and the two arrays are two different instances, the result is false.

[1,2,3] == [1,2,3] ==> false

All operands are non-primitive values, so + starts with the leftmost triggering numeric conversion. Both Object’s and Array’svalueOf method returns the object itself, so it’s ignored. toString() is used as a fallback. The trick here is that first {} is not considered as an object literal, but rather as a block declaration statement, so it’s ignored. Evaluation starts with next +[] expression, which is converted to an empty string via toString() method and then to 0 .

{}+[]+{}+[1] ==> +[]+{}+[1] ==> 0 + {} + [1] ==> 0 + '[object Object]' + [1] ==> '0[object Object]' + [1] ==> '0[object Object]' + '1' ==> '0[object Object]1'

This one is better explained step by step according to operator precedence.

!+[]+[]+![] ==> (!+[]) + [] + (![]) ==> !0 + [] + false ==> true + [] + false ==> true + '' + false ==> 'truefalse'

- operator triggers numeric conversion for Date. Date.valueOf() returns number of milliseconds since Unix epoch.

new Date(0) - 0 ==> 0 - 0 ==> 0

+ operator triggers default conversion. Date assumes string conversion as a default one, so toString() method is used, rather than valueOf().

new Date(0) + 0 ==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)' + 0 ==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)0'

Resources

I really want to recommend the excellent book “Understanding ES6” written by Nicholas C. Zakas. It’s a great ES6 learning resource, not too high-level, and does not dig into internals too much.

And here is a good book on ES5 only - SpeakingJS written by Axel Rauschmayer.

(Russian) Современный учебник Javascript — //learn.javascript.ru/. Especially these two pages on type coercion.

JavaScript Comparison Table — //dorey.github.io/JavaScript-Equality-Table/

wtfjs — a little code blog about that language we love despite giving us so much to hate — //wtfjs.com/