valueOf () vs. toString () in Javascript

115

In Javascript hat jedes Objekt eine valueOf () - und eine toString () -Methode. Ich hätte gedacht, dass die toString () -Methode immer dann aufgerufen wird, wenn eine Zeichenfolgenkonvertierung erforderlich ist, aber anscheinend wird sie von valueOf () übertrumpft.

Zum Beispiel der Code

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

wird gedruckt

x=42
x=foo

Das kommt mir rückwärts vor. Wenn x zum Beispiel eine komplexe Zahl wäre, würde ich wollen, dass valueOf () mir seine Größe gibt, aber wann immer ich in einen String konvertieren wollte, würde ich so etwas wie "a + bi" wollen. Und ich möchte nicht explizit toString () in Kontexten aufrufen müssen, die eine Zeichenfolge implizieren.

Ist das so wie es ist?

Brainjam
quelle
6
Hast du es versucht window.console.log (x);oder alert (x);?
Li0liQ
5
Sie geben "Object" bzw. "foo". Lustige Sachen.
Brainjam
Eigentlich Alarm (x); gibt "foo" und window.console.log (x) an; gibt "foo {}" in Firebug und das gesamte Objekt in der Chrome-Konsole an.
Brainjam
In Firefox 33.0.2 werden alert(x)Anzeigen foound window.console.log(x)Anzeigen angezeigt Object { toString: x.toString(), valueOf: x.valueOf() }.
John Sonderson

Antworten:

106

Der Grund, warum ("x =" + x) "x = Wert" und nicht "x = Zeichenfolge" ergibt, ist der folgende. Bei der Auswertung von "+" sammelt Javascript zuerst die Grundwerte der Operanden und entscheidet dann basierend auf dem Typ jedes Grundelements, ob Addition oder Verkettung angewendet werden soll.

So denkst du, funktioniert es

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

und genau das passiert tatsächlich

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))

Das heißt, toString wird auf das Ergebnis von valueOf angewendet, nicht auf Ihr ursprüngliches Objekt.

Weitere Informationen finden Sie in Abschnitt 11.6.1 Der Additionsoperator (+) in der ECMAScript-Sprachspezifikation.


* Wenn in String - Kontext genannt, ToPrimitive tut invoke toString, aber das ist hier nicht der Fall, weil ‚+‘ erzwingt keine Art Kontext.

user187291
quelle
3
Sollte die Bedingung im "tatsächlich" -Block nicht "if (pa is string && pb is string)" lauten? Dh "&&" statt "||" ?
Brainjam
3
Der Standard sagt definitiv "oder" (siehe Link).
user187291
2
Ja, das ist genau richtig - Zeichenfolgen haben bei der Verkettung Vorrang vor anderen Typen. Wenn einer der Operanden eine Zeichenfolge ist, wird das gesamte Objekt als Zeichenfolge verkettet. Gute Antwort.
devios1
76

Hier ist ein bisschen mehr Detail, bevor ich zur Antwort komme:

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"

Die toStringFunktion wird von nicht "übertrumpft"valueOf im Allgemeinen . Der ECMAScript-Standard beantwortet diese Frage tatsächlich ziemlich gut. Jedes Objekt verfügt über eine [[DefaultValue]]Eigenschaft, die bei Bedarf berechnet wird. Wenn der Interpreter nach dieser Eigenschaft fragt, gibt er auch einen "Hinweis" darauf, welche Art von Wert er erwartet. Wenn der Hinweis lautet String, toStringwird er zuvor verwendet valueOf. Wenn der Hinweis jedoch lautet Number, valueOfwird er zuerst verwendet. Beachten Sie, dass, wenn nur eines vorhanden ist oder ein nicht primitives Element zurückgegeben wird, normalerweise das andere als zweite Wahl aufgerufen wird.

Das + Operator gibt immer den Hinweis aus Number, auch wenn der erste Operand ein Zeichenfolgenwert ist. Obwohl es nach xseiner NumberDarstellung fragt [[DefaultValue]], führt es eine Zeichenfolgenverkettung durch , da der erste Operand eine Zeichenfolge von zurückgibt .

Wenn Sie sicherstellen möchten, dass toStringdie Zeichenfolgenverkettung aufgerufen wird, verwenden Sie ein Array und die .join("")Methode.

(ActionScript 3.0 ändert das Verhalten von +jedoch geringfügig . Wenn einer der Operanden a ist String, wird er als Zeichenfolgenverkettungsoperator behandelt und Stringbeim Aufrufen der Hinweis verwendet [[DefaultValue]]. In AS3 ergibt dieses Beispiel also "foo, x = foo, foo" = x, foo1, 43, x = foo ".)

Bcherry
quelle
1
Auch beachten , dass , wenn valueOfoder toStringRückkehr nicht-Primitiven, sie werden ignoriert. Wenn keines existiert oder keines ein Grundelement zurückgibt, TypeErrorwird a geworfen.
Bcherry
1
Danke bcherry, das ist das Kaliber der Antwort, auf das ich gehofft habe. Aber sollte nicht x + "x ="; Ausbeute "42x ="? Und x + "1"; Ausbeute 421? Haben Sie auch eine URL für den relevanten Teil des ECMAScript-Standards?
Brainjam
2
Tatsächlich verwendet '+' keine Hinweise (siehe $ 11.6.1), daher ruft ToPrimitive auf [[DefaultValue]](no-hint), was äquivalent zu ist [[DefaultValue]](number).
user187291
9
Dies scheint bei der integrierten Date-Klasse nicht der Fall zu sein. ("" + new Date(0)) === new Date(0).toString(). Ein Date-Objekt scheint immer seinen toString()Wert zurückzugeben, wenn es zu etwas hinzugefügt wird.
Kpozin
7
+1 & Danke! Ich habe Ihren Blog-Beitrag gefunden, in dem Sie auf diese Antwort näher eingehen, und wollte sie hier verlinken / teilen. Es war eine sehr hilfreiche Ergänzung zu dieser Antwort (einschließlich des Kommentars von Dmitry A. Soshnikov).
GitaarLAB
1

TLDR

Typzwang oder implizite Typkonvertierung ermöglicht eine schwache Typisierung und wird in JavaScript verwendet. Die meisten Operatoren (mit Ausnahme der strengen Gleichheitsoperatoren ===und !==) und Wertprüfungsoperationen (z. B. if(value)...) erzwingen die ihnen zugewiesenen Werte, wenn die Typen dieser Werte nicht sofort mit der Operation kompatibel sind.

Der genaue Mechanismus zum Erzwingen eines Werts hängt vom zu bewertenden Ausdruck ab. In der Frage wird der Additionsoperator verwendet.

Der Additionsoperator stellt zunächst sicher, dass beide Operanden Grundelemente sind. In diesem Fall wird die valueOfMethode aufgerufen. Die toStringMethode wird in diesem Fall nicht aufgerufen, da die überschriebene valueOfMethode für das Objekt xeinen primitiven Wert zurückgibt.

Da einer der Operanden in der Frage eine Zeichenfolge ist, werden beide Operanden in Zeichenfolgen konvertiert. Dieser Prozess verwendet die abstrakte interne Operation ToString(Anmerkung: groß geschrieben) und unterscheidet sich von der toStringMethode für das Objekt (oder seine Prototypkette).

Schließlich werden die resultierenden Zeichenfolgen verkettet.

Einzelheiten

Auf dem Prototyp jedes Konstruktorfunktionsobjekts, das jedem Sprachtyp in JavaScript entspricht (dh Number, BigInt, String, Boolean, Symbol und Object), gibt es zwei Methoden: valueOfund toString.

Der Zweck von valueOfbesteht darin, den primitiven Wert abzurufen, der einem Objekt zugeordnet ist (falls vorhanden). Wenn einem Objekt kein primitiver Wert zugrunde liegt, wird das Objekt einfach zurückgegeben.

Wenn valueOfes für ein Grundelement aufgerufen wird, wird das Grundelement auf normale Weise automatisch eingerahmt und der zugrunde liegende Grundelementwert zurückgegeben. Beachten Sie, dass für Zeichenfolgen der zugrunde liegende Grundwert (dh der von zurückgegebene Wert valueOf) die Zeichenfolgendarstellung selbst ist.

Der folgende Code zeigt, dass die valueOfMethode den zugrunde liegenden Grundwert von einem Wrapper-Objekt zurückgibt, und es zeigt, wie unveränderte Objektinstanzen, die nicht Grundelementen entsprechen, keinen Grundwert zurückgeben können, sodass sie sich einfach selbst zurückgeben.

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

Der Zweck von toStringist andererseits die Rückgabe einer Zeichenfolgendarstellung eines Objekts.

Beispielsweise:

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

Bei den meisten Vorgängen versucht JavaScript stillschweigend, einen oder mehrere Operanden in den erforderlichen Typ zu konvertieren. Dieses Verhalten wurde gewählt, um die Verwendung von JavaScript zu vereinfachen. JavaScript hatte anfangs keine Ausnahmen , und dies könnte auch bei dieser Entwurfsentscheidung eine Rolle gespielt haben. Diese Art der impliziten Typkonvertierung wird als Typenzwang bezeichnet und ist die Grundlage für das lose (schwache) JavaScript-Typsystem. Die komplizierten Regeln hinter diesem Verhalten sollen die Komplexität der Typisierung in die Sprache selbst und aus Ihrem Code heraus verschieben.

Während des Zwangsprozesses können zwei Arten der Umwandlung auftreten:

  1. Konvertierung eines Objekts in ein Grundelement (das möglicherweise eine Typkonvertierung selbst beinhaltet) und
  2. Direkte Umwandlung auf einen bestimmten Typ Beispiel ein Konstruktor Funktionsobjekt einer der primitiven Typen verwendet (dh. Number(), Boolean(), String()Etc.)

Umwandlung in ein Primitiv

Beim Versuch, nicht-primitive Typen in zu bearbeitende Primitive zu konvertieren, wird die abstrakte Operation ToPrimitivemit einem optionalen "Hinweis" auf "Zahl" oder "Zeichenfolge" aufgerufen. Wenn der Hinweis weggelassen wird, lautet der Standardhinweis 'number' (es sei denn, die @@toPrimitiveMethode wurde überschrieben). Wenn der Hinweis 'string' ist, toStringwird zuerst versucht, und valueOfzweitens, wenn toStringkein Grundelement zurückgegeben wurde. Sonst umgekehrt. Der Hinweis hängt von der Operation ab, die die Konvertierung anfordert.

Der Additionsoperator liefert keinen Hinweis, wird also valueOfzuerst versucht. Der Subtraktionsoperator gibt einen Hinweis auf 'Zahl' aus, wird also valueOfzuerst versucht. Die einzigen Situationen, die ich in der Spezifikation finden kann, in denen der Hinweis "Zeichenfolge" ist, sind:

  1. Object#toString
  2. Die abstrakte Operation ToPropertyKey, die ein Argument in einen Wert konvertiert, der als Eigenschaftsschlüssel verwendet werden kann

Direkte Typkonvertierung

Jeder Bediener hat seine eigenen Regeln für den Abschluss seines Vorgangs. Der Additionsoperator stellt zunächst ToPrimitivesicher, dass jeder Operand ein Grundelement ist. Wenn einer der Operanden eine Zeichenfolge ist, ruft er absichtlich die abstrakte Operation ToStringfür jeden Operanden auf, um das erwartete Verkettungsverhalten der Zeichenfolge mit Zeichenfolgen zu erzielen. Wenn nach dem ToPrimitiveSchritt beide Operanden keine Zeichenfolgen sind, wird eine arithmetische Addition durchgeführt.

Im Gegensatz zur Addition hat der Subtraktionsoperator kein überladenes Verhalten und ruft daher toNumericjeden Operanden auf, der sie zuerst mit in Primitive konvertiert hat ToPrimitive.

So:

 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

Beachten Sie, dass das Dateintrinsische Objekt eindeutig ist, da es das einzige intrinsische Objekt ist, @@toPrimitivedas die Standardmethode überschreibt , bei der angenommen wird, dass der Standardhinweis "Zeichenfolge" (anstelle von "Zahl") ist. Der Grund dafür ist, dass DateInstanzen zur Vereinfachung für den Programmierer standardmäßig anstelle ihres numerischen Werts in lesbare Zeichenfolgen übersetzt werden. Sie können @@toPrimitiveIhre eigenen Objekte mit überschreiben Symbol.toPrimitive.

Das folgende Raster zeigt die Zwangsergebnisse für den abstrakten Gleichheitsoperator ( ==) ( Quelle ):

Geben Sie hier die Bildbeschreibung ein

Siehe auch .

Ben Aston
quelle