valueOf () vs toString () en Javascript


115

En Javascript, chaque objet a une méthode valueOf () et toString (). J'aurais pensé que la méthode toString () était invoquée chaque fois qu'une conversion de chaîne est demandée, mais apparemment, elle est remplacée par valueOf ().

Par exemple, le code

var x = {toString: function() {return "foo"; },
         valueOf: function() {return 42; }};
window.console.log ("x="+x);
window.console.log ("x="+x.toString());

va imprimer

x=42
x=foo

Cela me semble à l'envers .. si x était un nombre complexe, par exemple, je voudrais que valueOf () me donne sa grandeur, mais chaque fois que je veux convertir en une chaîne, je voudrais quelque chose comme "a + bi". Et je ne voudrais pas avoir à appeler toString () explicitement dans des contextes qui impliquent une chaîne.

Est-ce juste comme ça?


6
Avez-vous essayé window.console.log (x);ou alert (x);?
Li0liQ

5
Ils donnent respectivement "Object" et "foo". Truc amusant.
brainjam

En fait, alert (x); donne "toto" et window.console.log (x); donne "foo {}" dans Firebug et l'objet entier dans la console Chrome.
brainjam

Dans Firefox 33.0.2 alert(x)s'affiche fooet window.console.log(x)affiche Object { toString: x.toString(), valueOf: x.valueOf() }.
John Sonderson

Réponses:


107

La raison pour laquelle ("x =" + x) donne "x = valeur" et non "x = tostring" est la suivante. Lors de l'évaluation de "+", javascript collecte d'abord les valeurs primitives des opérandes, puis décide si l'addition ou la concaténation doit être appliquée, en fonction du type de chaque primitive.

Alors, c'est comme ça que tu penses que ça marche

a + b:
    pa = ToPrimitive(a)
    if(pa is string)
       return concat(pa, ToString(b))
    else
       return add(pa, ToNumber(b))

et c'est ce qui se passe réellement

a + b:
    pa = ToPrimitive(a)
    pb = ToPrimitive(b)*
    if(pa is string || pb is string)
       return concat(ToString(pa), ToString(pb))
    else
       return add(ToNumber(pa), ToNumber(pb))

Autrement dit, toString est appliqué au résultat de valueOf, pas à votre objet d'origine.

Pour plus d'informations, consultez la section 11.6.1 L'opérateur Addition (+) dans la spécification du langage ECMAScript.


* Appelé dans un contexte de chaîne, ToPrimitive ne Invoke toString, mais ce n'est pas le cas ici, parce que « + » n'impose pas de contexte de type.


3
Le conditionnel dans le bloc "en fait" ne devrait-il pas lire "si (pa est une chaîne && pb est une chaîne)"? Ie "&&" au lieu de "||" ?
brainjam

3
La norme dit définitivement "ou" (voir le lien).
user187291

2
Oui, c'est exactement ça - la priorité est donnée aux chaînes par rapport aux autres types dans la concaténation. Si l' un des opérandes est une chaîne, le tout sera concaténé en tant que chaîne. Bonne réponse.
devios1

76

Voici un peu plus de détails, avant d'arriver à la réponse:

var x = {
    toString: function () { return "foo"; },
    valueOf: function () { return 42; }
};

alert(x); // foo
"x=" + x; // "x=42"
x + "=x"; // "42=x"
x + "1"; // 421
x + 1; // 43
["x=", x].join(""); // "x=foo"

La toStringfonction n'est pas "détruite" par valueOfen général. Le standard ECMAScript répond en fait assez bien à cette question. Chaque objet a une [[DefaultValue]]propriété, qui est calculée à la demande. Lorsqu'il demande cette propriété, l'interpréteur fournit également un "indice" pour le type de valeur qu'il attend. Si l'indice est String, alors toStringest utilisé avant valueOf. Mais, si l'indice est Number, alors valueOfsera utilisé en premier. Notez que si un seul est présent, ou s'il renvoie un non primitif, il appellera généralement l'autre comme deuxième choix.

L' +opérateur fournit toujours l'indication Number, même si le premier opérande est une valeur de chaîne. Même s'il demande xsa Numberreprésentation, puisque le premier opérande renvoie une chaîne de [[DefaultValue]], il effectue une concaténation de chaînes.

Si vous voulez garantir qu'il toStringest appelé pour la concaténation de chaînes, utilisez un tableau et la .join("")méthode.

( +Cependant, ActionScript 3.0 modifie légèrement le comportement de . Si l'un des opérandes est a String, il le traitera comme un opérateur de concaténation de chaînes et utilisera l'indicateur Stringlors de son appel [[DefaultValue]]. Ainsi, dans AS3, cet exemple donne "foo, x = foo, foo = x, toto1, 43, x = toto ".)


1
Notez également que si valueOfou toStringretourne des non-primitives, elles sont ignorées. Si aucun des deux n’existe ou ne renvoie une primitive, alors a TypeErrorest renvoyé.
bcherry

1
Merci bcherry, c'est le calibre de réponse que j'espérais. Mais ne devrait pas x + "x ="; rendement "42x ="? Et x + "1"; rendement 421? Avez-vous également une URL pour la partie pertinente de la norme ECMAScript?
brainjam

2
En fait, '+' n'utilise pas d'indices (voir $ 11.6.1) donc ToPrimitive invoque [[DefaultValue]](no-hint), ce qui équivaut à [[DefaultValue]](number).
user187291

9
Cela ne semble pas être le cas pour la classe Date intégrée. ("" + new Date(0)) === new Date(0).toString(). Un objet Date semble toujours renvoyer sa toString()valeur lorsqu'il est ajouté à quelque chose.
kpozin le

7
+1 et merci! J'ai trouvé votre article de blog dans lequel vous développez cette réponse et je voulais le lier / le partager ici. C'était un ajout vraiment utile à cette réponse (y compris le commentaire de Dmitry A. Soshnikov).
GitaarLAB

1

TLDR

La coercition de type, ou conversion de type implicite, permet un typage faible et est utilisée dans tout JavaScript. La plupart des opérateurs (à l'exception notable des opérateurs d'égalité stricte ===et !==) et des opérations de vérification de valeur (par exemple if(value)...) contraindront les valeurs qui leur sont fournies, si les types de ces valeurs ne sont pas immédiatement compatibles avec l'opération.

Le mécanisme précis utilisé pour forcer une valeur dépend de l'expression évaluée. Dans la question, l' opérateur d'addition est utilisé.

L'opérateur d'addition s'assurera d'abord que les deux opérandes sont des primitives, ce qui, dans ce cas, implique l'appel de la valueOfméthode. La toStringméthode n'est pas appelée dans cette instance car la valueOfméthode surchargée sur l'objet xrenvoie une valeur primitive.

Ensuite, comme l'un des opérandes de la question est une chaîne, les deux opérandes sont convertis en chaînes. Ce processus utilise l'opération abstraite, interne ToString(note: en majuscule), et est distinct de la toStringméthode sur l'objet (ou sa chaîne prototype).

Enfin, les chaînes résultantes sont concaténées.

Détails

Sur le prototype de chaque objet de fonction constructeur correspondant à chaque type de langage en JavaScript (c'est-à-dire Number, BigInt, String, Boolean, Symbol et Object), il existe deux méthodes: valueOfet toString.

Le but de valueOfest de récupérer la valeur primitive associée à un objet (s'il en a une). Si un objet n'a pas de valeur primitive sous-jacente, l'objet est simplement renvoyé.

Si valueOfest invoqué contre une primitive, alors la primitive est automatiquement encadrée de la manière normale et la valeur primitive sous-jacente est renvoyée. Notez que pour les chaînes, la valeur primitive sous-jacente (c'est-à-dire la valeur renvoyée par valueOf) est la représentation sous forme de chaîne elle-même.

Le code suivant montre que la valueOfméthode renvoie la valeur primitive sous-jacente à partir d'un objet wrapper et montre comment les instances d'objet non modifiées qui ne correspondent pas aux primitives n'ont pas de valeur primitive à renvoyer, elles se renvoient donc simplement elles-mêmes.

console.log(typeof new Boolean(true)) // 'object'
console.log(typeof new Boolean(true).valueOf()) // 'boolean'
console.log(({}).valueOf()) // {} (no primitive value to return)

Le but de toString, d'autre part, est de retourner une représentation sous forme de chaîne d'un objet.

Par exemple:

console.log({}.toString()) // '[object Object]'
console.log(new Number(1).toString()) // '1'

Pour la plupart des opérations, JavaScript tentera silencieusement de convertir un ou plusieurs opérandes dans le type requis. Ce comportement a été choisi pour faciliter l'utilisation de JavaScript. Au départ, JavaScript n'avait pas d'exceptions , et cela a peut-être également joué un rôle dans cette décision de conception. Ce type de conversion de type implicite est appelé coercition de type, et c'est la base du système de type lâche (faible) de JavaScript. Les règles compliquées derrière ce comportement visent à déplacer la complexité de la conversion de type dans le langage lui-même et hors de votre code.

Pendant le processus coercitif, deux modes de conversion peuvent se produire:

  1. Conversion d'un objet en primitive (qui peut impliquer une conversion de type elle-même), et
  2. La conversion directe à une instance de type spécifique, en utilisant un objet fonction constructeur d'un des types primitifs (ie. Number(), Boolean(), String()Etc.)

Conversion en primitive

Lors de la tentative de conversion de types non primitifs en primitives sur lesquelles opérer, l'opération abstraite ToPrimitiveest appelée avec un "indice" facultatif de "nombre" ou "chaîne". Si le conseil est omis, le conseil par défaut est 'number' (sauf si la @@toPrimitiveméthode a été remplacée). Si l'indice est 'string', alors toStringest essayé en premier, et en valueOfsecond lieu si toStringn'a pas renvoyé de primitive. Sinon, vice-versa. L'indication dépend de l'opération qui demande la conversion.

L'opérateur d'addition ne fournit aucun indice, il valueOfest donc essayé en premier. L'opérateur de soustraction fournit un indice de «nombre», donc valueOfest essayé en premier. Les seules situations que je peux trouver dans la spécification dans lesquelles l'indication est 'string' sont:

  1. Object#toString
  2. L'opération abstraite ToPropertyKey, qui convertit un argument en une valeur qui peut être utilisée comme clé de propriété

Conversion directe de type

Chaque opérateur a ses propres règles pour terminer son opération. L'opérateur d'addition utilisera d'abord ToPrimitivepour s'assurer que chaque opérande est une primitive; puis, si l'un des opérandes est une chaîne, il invoquera alors délibérément l'opération abstraite ToStringsur chaque opérande, pour fournir le comportement de concaténation de chaînes que nous attendons avec les chaînes. Si, après l' ToPrimitiveétape, les deux opérandes ne sont pas des chaînes, une addition arithmétique est effectuée.

Contrairement à l'addition, l'opérateur de soustraction n'a pas de comportement surchargé, et donc invoquera toNumericsur chaque opérande les ayant d'abord convertis en primitives en utilisant ToPrimitive.

Alors:

 1  +  1   //  2                 
'1' +  1   // '11'   Both already primitives, RHS converted to string, '1' + '1',   '11'
 1  + [2]  // '12'   [2].valueOf() returns an object, so `toString` fallback is used, 1 + String([2]), '1' + '2', 12
 1  + {}   // '1[object Object]'    {}.valueOf() is not a primitive, so toString fallback used, String(1) + String({}), '1' + '[object Object]', '1[object Object]'
 2  - {}   // NaN    {}.valueOf() is not a primitive, so toString fallback used => 2 - Number('[object Object]'), NaN
+'a'       // NaN    `ToPrimitive` passed 'number' hint), Number('a'), NaN
+''        // 0      `ToPrimitive` passed 'number' hint), Number(''), 0
+'-1'      // -1     `ToPrimitive` passed 'number' hint), Number('-1'), -1
+{}        // NaN    `ToPrimitive` passed 'number' hint', `valueOf` returns an object, so falls back to `toString`, Number('[Object object]'), NaN
 1 + 'a'   // '1a'    Both are primitives, one is a string, String(1) + 'a'
 1 + {}    // '1[object Object]'    One primitive, one object, `ToPrimitive` passed no hint, meaning conversion to string will occur, one of the operands is now a string, String(1) + String({}), `1[object Object]`
[] + []    // ''     Two objects, `ToPrimitive` passed no hint, String([]) + String([]), '' (empty string)
 1 - 'a'   // NaN    Both are primitives, one is a string, `ToPrimitive` passed 'number' hint, 1-Number('a'), 1-NaN, NaN
 1 - {}    // NaN    One primitive, one is an object, `ToPrimitive` passed 'number' hint, `valueOf` returns object, so falls back to `toString`, 1-Number([object Object]), 1-NaN, NaN
[] - []    // 0      Two objects, `ToPrimitive` passed 'number' hint => `valueOf` returns array instance, so falls back to `toString`, Number('')-Number(''), 0-0, 0

Notez que l' Dateobjet intrinsèque est unique, en ce sens qu'il est le seul intrinsèque à remplacer la @@toPrimitiveméthode par défaut , dans laquelle l'indice par défaut est présumé être «chaîne» (plutôt que «nombre»). La raison en est que les Dateinstances se traduisent par défaut en chaînes lisibles, au lieu de leur valeur numérique, pour la commodité du programmeur. Vous pouvez remplacer @@toPrimitivevos propres objets en utilisant Symbol.toPrimitive.

La grille suivante montre les résultats de la coercition pour l'opérateur d'égalité abstraite ( ==) ( source ):

entrez la description de l'image ici

Voir aussi .

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.