Que se passe-t-il lorsque des objets sont ajoutés obj1 + obj2
, soustraits obj1 - obj2
ou imprimés à l'aide alert(obj)
?
JavaScript ne vous permet pas de personnaliser la façon dont les opérateurs travaillent sur les objets. Contrairement à certains autres langages de programmation, tels que Ruby ou C++, nous ne pouvons pas implémenter de méthode objet spéciale pour gérer l'addition (ou d'autres opérateurs).
Dans le cas de telles opérations, les objets sont automatiquement convertis en primitives, puis l'opération est effectuée sur ces primitives et aboutit à une valeur primitive.
C'est une limitation importante : le résultat de obj1 + obj2
(ou une autre opération mathématique) ne peut pas être un autre objet !
Par exemple, nous ne pouvons pas créer des objets représentant des vecteurs ou des matrices (ou des réalisations ou autre), les ajouter et espérer un objet « additionné » comme résultat. De telles prouesses architecturales sont automatiquement « exclues ».
Donc, comme nous ne pouvons techniquement pas faire grand-chose ici, il n'y a pas de mathématiques avec des objets dans des projets réels. Lorsque cela arrive, à de rares exceptions près, c'est à cause d'une erreur de codage.
Dans ce chapitre, nous verrons comment un objet se convertit en primitif et comment le personnaliser.
Nous avons deux objectifs :
Date
). Nous les retrouverons plus tard.Dans le chapitre Conversions de types, nous avons vu les règles pour les conversions numériques, de chaînes et booléennes des primitives. Mais nous avons laissé un espace pour les objets. Maintenant que nous connaissons les méthodes et les symboles, il devient possible de le remplir.
true
dans un contexte booléen, aussi simple que cela. Il n'existe que des conversions numériques et de chaînes.Date
(à traiter dans le chapitre Date et heure) peuvent être soustraits, et le résultat de date1 - date2
est la différence de temps entre deux dates.alert(obj)
et dans des contextes similaires.Nous pouvons implémenter nous-mêmes la conversion de chaînes et de valeurs numériques, en utilisant des méthodes d'objet spéciales.
Passons maintenant aux détails techniques, car c'est le seul moyen d'aborder le sujet en profondeur.
Comment JavaScript décide-t-il quelle conversion appliquer ?
Il existe trois variantes de conversion de type, qui se produisent dans diverses situations. Ils sont appelés « indices », comme décrit dans la spécification :
"string"
Pour une conversion objet en chaîne, lorsque nous effectuons une opération sur un objet qui attend une chaîne, comme alert
:
// output alert(obj); // using object as a property key anotherObj[obj] = 123;
"number"
Pour une conversion objet en nombre, comme lorsque nous faisons des mathématiques :
// explicit conversion let num = Number(obj); // maths (except binary plus) let n = +obj; // unary plus let delta = date1 - date2; // less/greater comparison let greater = user1 > user2;
La plupart des fonctions mathématiques intégrées incluent également une telle conversion.
"default"
Se produit dans de rares cas où l’opérateur « ne sait pas » à quel type s’attendre.
Par exemple, binaire plus +
peut fonctionner à la fois avec des chaînes (les concatène) et des nombres (les ajoute). Ainsi, si un binaire plus obtient un objet comme argument, il utilise l'indice "default"
pour le convertir.
De plus, si un objet est comparé en utilisant ==
avec une chaîne, un nombre ou un symbole, il n'est pas non plus clair quelle conversion doit être effectuée, donc l'indice "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 plus grand et moins, tels que <
>
, peuvent également fonctionner avec des chaînes et des nombres. Pourtant, ils utilisent l'indice "number"
, pas "default"
. C'est pour des raisons historiques.
Mais en pratique, les choses sont un peu plus simples.
Tous les objets intégrés, à l'exception d'un cas (objet Date
, nous l'apprendrons plus tard) implémentent la conversion "default"
de la même manière que "number"
. Et nous devrions probablement faire de même.
Pourtant, il est important de connaître les 3 indices, nous verrons bientôt pourquoi.
Pour effectuer la conversion, JavaScript essaie de trouver et d'appeler trois méthodes objet :
obj[Symbol.toPrimitive](hint)
– la méthode avec la clé symbolique Symbol.toPrimitive
(symbole système), si une telle méthode existe,"string"
obj.toString()
ou obj.valueOf()
, tout ce qui existe."number"
ou "default"
obj.valueOf()
ou obj.toString()
, tout ce qui existe. Commençons par la première méthode. Il existe un symbole intégré nommé Symbol.toPrimitive
qui doit être utilisé pour nommer la méthode de conversion, comme ceci :
obj[Symbol.toPrimitive] = function(hint) { // here goes the code to convert this object to a primitive // it must return a primitive value // hint = one of "string", "number", "default" };
Si la méthode Symbol.toPrimitive
existe, elle est utilisée pour tous les indices et aucune autre méthode n'est nécessaire.
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 nous pouvons le voir dans le code, user
devient une chaîne 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.
S'il n'y a pas de Symbol.toPrimitive
alors JavaScript essaie de trouver les méthodes toString
et valueOf
:
"string"
: appelez la méthode toString
, et si elle n'existe pas ou si elle renvoie un objet au lieu d'une valeur primitive, alors appelez valueOf
(donc toString
a la priorité pour les conversions de chaînes).valueOf
, et s'il n'existe pas ou s'il renvoie un objet au lieu d'une valeur primitive, appelez toString
(donc valueOf
a la priorité pour les mathématiques). Les méthodes toString
et valueOf
proviennent des temps anciens. Ce ne sont pas des symboles (les symboles n’existaient pas il y a si longtemps), mais plutôt des méthodes « normales » nommées par chaîne. Ils fournissent une manière alternative « à l’ancienne » de mettre en œuvre la conversion.
Ces méthodes doivent renvoyer une valeur primitive. Si toString
ou valueOf
renvoie un objet, alors il est ignoré (comme s'il n'y avait pas de méthode).
Par défaut, un objet simple a les méthodes toString
et valueOf
suivantes :
toString
renvoie une chaîne "[object Object]"
.valueOf
renvoie l'objet lui-même.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 comme chaîne, comme dans une alert
, alors par défaut nous voyons [object Object]
.
La valueOf
par défautOf est mentionnée ici uniquement par souci d'exhaustivité, pour éviter toute confusion. Comme vous pouvez le voir, il renvoie l'objet lui-même et est donc ignoré. Ne me demandez pas pourquoi, c'est pour des raisons historiques. On peut donc supposer que cela n'existe pas.
Implémentons ces méthodes pour personnaliser la conversion.
Par exemple, ici, user
fait la même chose que ci-dessus en utilisant une combinaison de toString
et valueOf
au lieu de Symbol.toPrimitive
:
let user = { name: "John", money: 1000, // for hint="string" toString() { return `{name: "${this.name}"}`; }, // for hint="number" or "default" valueOf() { return this.money; } }; alert(user); // toString -> {name: "John"} alert(+user); // valueOf -> 1000 alert(user + 500); // valueOf -> 1500
Comme on peut le voir, le comportement est le même que l'exemple précédent avec Symbol.toPrimitive
.
Souvent, nous souhaitons un seul endroit « fourre-tout » pour gérer toutes les conversions primitives. Dans ce cas, nous pouvons implémenter uniquement toString
, 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 valueOf
, toString
gérera toutes les conversions primitives.
La chose importante à savoir sur toutes les méthodes de conversion de primitives est qu'elles ne renvoient pas nécessairement la primitive « suggérée ».
Il n'y a aucun contrôle si toString
renvoie exactement une chaîne ou si la méthode Symbol.toPrimitive
renvoie un nombre pour l'indice "number"
.
Seule chose obligatoire : ces méthodes doivent renvoyer une primitive, pas un objet.
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 dans les temps anciens, il n'existait pas de bon concept « d'erreur » en JavaScript.
En revanche, Symbol.toPrimitive
est plus strict, il doit renvoyer une primitive, sinon il y aura une erreur.
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 on passe un objet en argument, alors il y a deux étapes de calcul :
Par exemple:
let obj = { // toString handles all conversions in the absence of other methods toString() { return "2"; } }; alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number
obj * 2
convertit d'abord l'objet en primitif (c'est une chaîne "2"
)."2" * 2
devient 2 * 2
(la chaîne est convertie en nombre).Binary plus concatènera les chaînes dans la même situation, car il accepte volontiers une chaîne :
let obj = { toString() { return "2"; } }; alert(obj + 2); // "22" ("2" + 2), conversion to primitive returned a string => concatenation
La conversion objet en primitive est appelée automatiquement par de nombreuses fonctions et opérateurs intégrés qui attendent une primitive comme valeur.
Il en existe 3 types (indices) :
"string"
(pour alert
et autres opérations nécessitant une chaîne)"number"
(pour les mathématiques)"default"
(quelques opérateurs, généralement des objets l'implémentent de la même manière que "number"
)La spécification décrit explicitement quel opérateur utilise quel indice.
L'algorithme de conversion est le suivant :
obj[Symbol.toPrimitive](hint)
si la méthode existe,"string"
obj.toString()
ou obj.valueOf()
, tout ce qui existe."number"
ou "default"
obj.valueOf()
ou obj.toString()
, tout ce qui existe.Toutes ces méthodes doivent renvoyer une primitive au travail (si définie).
En pratique, il suffit souvent d'implémenter uniquement obj.toString()
comme méthode « fourre-tout » pour les conversions de chaînes qui doivent renvoyer une représentation « lisible par l'homme » d'un objet, à des fins de journalisation ou de débogage.