16 août 2020

Conversion d'objet en primitive

Que se passe-t-il lorsque des objets sont ajoutés obj1 + obj2, soustraits obj1 - obj2 ou imprimés à l’aide de alert (obj) ?

Dans ce cas, les objets sont automatiquement convertis en primitives, puis l’opération est effectuée.

Dans le chapitre Les conversions de types nous avons vu les règles pour les conversions numériques, chaînes et booléennes de primitives. Mais nous avions mis de côté les objets. Maintenant que nous connaissons les méthodes et les symboles, il devient possible de l’aborder.

Pour les objets, il n’y a pas de conversion to-boolean, car tous les objets sont true dans un contexte booléen. Il n’y a donc que des conversions de chaînes de caractères et de chiffres.

  1. Tous les objets sont true dans un contexte booléen. Il n’y a que des conversions numériques et de chaînes de caractères.
  2. La conversion numérique se produit lorsque nous soustrayons des objets ou appliquons des fonctions mathématiques. Par exemple, les objets Date (à traiter dans le chapitre Date et Temps) peut être soustrait et le résultat de date1 - date2 est la différence de temps entre deux dates.
  3. En ce qui concerne la conversion de chaîne de caractères – cela se produit généralement lorsque nous affichons un objet tel que alert (obj) et dans des contextes similaires.

ToPrimitive

Nous pouvons affiner la conversion de chaînes de caractères et de chiffres en utilisant des méthodes d’objet spéciales.

Il existe trois variantes de conversion de type, appelées “hints”, décrites dans la specification :

"string"

Pour une conversion d’un objet vers une chaîne de caractères, lorsque nous effectuons une opération sur un objet qui attend une chaîne, comme alert :

// output
alert(obj);

// utiliser un objet comme clé de propriété
anotherObj[obj] = 123;

"number"

Pour une conversion d’objet en nombre, comme lorsque nous faisons des calculs :

// conversion explicite
let num = Number(obj);

// maths (except binary plus)
let n = +obj; // unary plus
let delta = date1 - date2;

// comparaison supérieur/inférieur
let greater = user1 > user2;
"default"

Se produit dans de rares cas où l’opérateur n’est “pas sûr” du type auquel il doit s’attendre.

Par exemple, le binaire plus + peut fonctionner à la fois avec des chaînes de caractères (les concaténer) et des nombres (les ajouter), donc les chaînes de caractères et les chiffres feraient l’affaire. Donc, si le plus binaire obtient un objet sous forme d’argument, il utilise l’indicateur "default" pour le convertir.

En outre, si un objet est comparé à l’aide de == avec une chaîne de caractères, un nombre ou un symbole, il est également difficile de savoir quelle conversion doit être effectuée, par conséquent l’indicateur "default" est utilisé.

// binary plus uses the "default" hint
let total = obj1 + obj2;

// obj == number uses the "default" hint
if (user == 1) { ... };

Les opérateurs de comparaison supérieurs et inférieurs, tels que < >, peuvent également fonctionner avec des chaînes de caractères et des nombres. Néanmoins, ils utilisent l’indicateur "number", pas default. C’est pour des raisons historiques,

En pratique cependant, nous n’avons pas besoin de nous souvenir de ces détails particuliers, car tous les objets intégrés, à l’exception d’un cas (l’objet Date, nous l’apprendrons plus tard), implémentent la conversion 'default' de la même manière que "number". Et nous pouvons faire la même chose.

Pas d’indice "boolean"

Veuillez noter qu’il n’y a que trois indices. C’est aussi simple que cela.

Il n’y a pas d’indice “boolean” (tous les objets sont true dans un contexte booléen) ou autre chose. Et si nous traitons de la même manière 'default' et 'number', comme le font la plupart des programmes intégrés, il n’y a au final que deux conversions.

Pour effectuer la conversion, JavaScript essaie de trouver et d’appeler trois méthodes d’objet :

  1. Appeler obj[Symbol.toPrimitive](hint) – la méthode avec la clé symbolique Symbol.toPrimitive (symbole système), si une telle méthode existe,
  2. Sinon, si l’indice est "string"
    • essaie obj.toString() et obj.valueOf(), tout ce qui existe.
  3. Sinon, si l’indice est "number" ou "default"
    • essaie obj.valueOf() et obj.toString(), tout ce qui existe.

Symbol.toPrimitive

Commençons par la première méthode. Il existe un symbole intégré appelé Symbol.toPrimitive qui devrait être utilisé pour nommer la méthode de conversion, comme ceci :

obj[Symbol.toPrimitive] = function(hint) {
  // doit renvoyer une valeur primitive
  // hint = un parmi "string", "number", "default"
};

Par exemple, ici l’objet user l’implémente :

let user = {
  name: "John",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    alert(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};

// conversions demo:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500

Comme on peut le voir d’après le code, user devient une chaîne de caractères auto-descriptive ou un montant d’argent en fonction de la conversion. La méthode unique user[Symbol.toPrimitive] gère tous les cas de conversion.

toString/valueOf

Méthodes toString et valueOf proviennent des temps anciens. Ce ne sont pas des symboles (les symboles n’existaient pas il n’y a pas si longtemps), mais plutôt des méthodes “régulières” avec des noms de chaînes de caractères. Ils fournissent une méthode alternative “à l’ancienne” pour implémenter la conversion.

S’il n’y a pas de Symbol.toPrimitive, alors JavaScript essaye de les trouver et essaie dans l’ordre :

  • toString -> valueOf pour le hint “string”.
  • valueOf -> toString sinon.

Ces méthodes doivent renvoyer une valeur primitive. Si toString ou valueOf renvoie un objet, il est ignoré (comme s’il n’y avait pas de méthode).

Par défaut, un objet brut a les méthodes toString et valueOf suivantes :

  • La méthode toString renvoie une chaîne de caractères "[object Object]".
  • La méthode valueOf renvoie l’objet en question.

Voici la démo :

let user = {name: "John"};

alert(user); // [object Object]
alert(user.valueOf() === user); // true

Donc, si nous essayons d’utiliser un objet en tant que chaîne de caractères, comme dans un alert ou autre chose, nous voyons par défaut [object Object].

Et la valeur par défaut valueOf n’est mentionnée ici que par souci d’exhaustivité, afin d’éviter toute confusion. Comme vous pouvez le constater, l’objet est renvoyé et est donc ignoré. Ne me demandez pas pourquoi, c’est pour des raisons historiques. Nous pouvons donc supposer que cela n’existe pas.

Implémentons ces méthodes.

Par exemple, ici, user fait la même chose que ci-dessus en combinant toString et valueOf au lieu de Symbol.toPrimitive :

let user = {
  name: "John",
  money: 1000,

  // for hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },

  // pour hint="number" ou "default"
  valueOf() {
    return this.money;
  }

};

alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500

Comme on peut le constater, le comportement est identique à celui de l’exemple précédent avec Symbol.toPrimitive.

Nous voulons souvent un seul endroit “fourre-tout” pour gérer toutes les conversions primitives. Dans ce cas, nous pouvons implémenter toString uniquement, comme ceci :

let user = {
  name: "John",

  toString() {
    return this.name;
  }
};

alert(user); // toString -> John
alert(user + 500); // toString -> John500

En l’absence de Symbol.toPrimitive et de valueOf, toString gérera toutes les conversions primitives.

Retourner des types

La chose importante à savoir sur toutes les méthodes de conversion de primitives est qu’elles ne renvoient pas nécessairement la primitive “hinted”.

Il n’y a pas de control pour vérifier si ToString() renvoie exactement une chaîne de caractères ou si la méthode Symbol.toPrimitive renvoie un nombre pour un indice "number".

La seule chose obligatoire : ces méthodes doivent renvoyer une primitive, pas un objet.

Notes historiques

Pour des raisons historiques, si toString ou valueOf renvoie un objet, il n’y a pas d’erreur, mais cette valeur est ignorée (comme si la méthode n’existait pas). C’est parce que jadis, il n’existait pas de bon concept “d’erreur” en JavaScript.

En revanche, Symbol.toPrimitive doit renvoyer une primitive, sinon une erreur se produira.

Autres conversions

Comme nous le savons déjà, de nombreux opérateurs et fonctions effectuent des conversions de types, par exemple la multiplication * convertit les opérandes en nombres.

Si nous passons un objet en argument, il y a deux étapes :

  1. L’objet est converti en primitive (en utilisant les règles décrites ci-dessus).
  2. Si la primitive résultante n’est pas du bon type, elle est convertie.

Par exemple :

let obj = {
  // toString gère toutes les conversions en l'absence d'autres méthodes
  toString() {
    return "2";
  }
};

alert(obj * 2); // 4, objet converti en primitive "2", puis la multiplication le transforme en un nombre
  1. La multiplication obj * 2 convertit d’abord l’objet en primitive (cela devient une chaîne de caractère "2").
  2. Ensuite "2" * 2 devient 2 * 2 (la chaîne de caractères est convertie en nombre).

Le binaire plus va concaténer des chaînes de caractères dans la même situation, car il accepte volontiers une chaîne de caractères :

let obj = {
  toString() {
    return "2";
  }
};

alert(obj + 2); // 22 ("2" + 2), la conversion en primitive a renvoyé une chaîne de caractères => concaténation

Résumé

La conversion objet à primitive est appelée automatiquement par de nombreuses fonctions intégrées et opérateurs qui attendent une primitive en tant que valeur.

Il en existe 3 types (hints) :

  • [ ] "string" (pour alert et d’autres opérations qui nécessitent une chaîne de caractères)
  • "number" (pour des maths)
  • "default" (peu d’opérateurs)

La spécification décrit explicitement quel opérateur utilise quel indice (hint). Très peu d’opérateurs “ne savent pas à quoi s’attendre” et utilisent l’indice "default". Habituellement, pour les objets intégrés, l’indice "default" est traité de la même façon que "number", de sorte qu’en pratique, les deux derniers sont souvent fusionnés.

L’algorithme de conversion est :

  1. Appeler obj[Symbol.toPrimitive](hint) si la méthode existe,
  2. Sinon, si l’indice est "string"
    • essaie obj.toString() et obj.valueOf(), tout ce qui existe.
  3. Sinon, si l’indice est "number" ou "default"
    • essaie obj.valueOf() et obj.toString(), tout ce qui existe.

En pratique, il suffit souvent d’implémenter uniquement obj.toString() en tant que méthode “fourre-tout” pour toutes les conversions qui renvoient une représentation “lisible par l’homme” d’un objet, à des fins de journalisation ou de débogage.

Carte du tutoriel

Commentaires

lire ceci avant de commenter…
  • Si vous avez des améliorations à suggérer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de préciser.
  • Pour insérer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes -- enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen…)