valueOf () rispetto a toString () in Javascript


115

In Javascript ogni oggetto ha un metodo valueOf () e toString (). Avrei pensato che il metodo toString () venisse invocato ogni volta che viene richiesta una conversione di stringhe, ma a quanto pare è superato da valueOf ().

Ad esempio, il codice

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

stamperà

x=42
x=foo

Questo mi sembra al contrario .. se x fosse un numero complesso, per esempio, vorrei che valueOf () mi desse la sua grandezza, ma ogni volta che volevo convertirlo in una stringa vorrei qualcosa come "a + bi". E non vorrei dover chiamare esplicitamente toString () in contesti che implicano una stringa.

È solo così?


6
Hai provato window.console.log (x);o alert (x);?
Li0liQ

5
Danno rispettivamente "Object" e "foo". Cose divertenti.
brainjam

In realtà, alert (x); dà "foo" e window.console.log (x); dà "pippo {}" in Firebug e l'intero oggetto nella console di Chrome.
brainjam

In Firefox 33.0.2 alert(x)vengono visualizzati fooe window.console.log(x)visualizzati Object { toString: x.toString(), valueOf: x.valueOf() }.
John Sonderson

Risposte:


107

Il motivo per cui ("x =" + x) restituisce "x = value" e non "x = tostring" è il seguente. Quando si valuta "+", javascript raccoglie prima i valori primitivi degli operandi, quindi decide se applicare l'addizione o la concatenazione, in base al tipo di ciascuna primitiva.

Quindi, questo è come pensi che funzioni

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

e questo è ciò che accade realmente

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

Ovvero, toString viene applicato al risultato di valueOf, non all'oggetto originale.

Per ulteriore riferimento, consultare la sezione 11.6.1 L'operatore di aggiunta (+) nella specifica del linguaggio ECMAScript.


* Quando viene chiamato in un contesto stringa, ToPrimitive fa invocare toString, ma questo non è il caso qui, perche '+' non far rispettare qualsiasi contesto tipo.


3
Il condizionale nel blocco "effettivamente" non dovrebbe leggere "if (pa è una stringa && pb è una stringa)"? Ad esempio "&&" invece di "||" ?
brainjam

3
Lo standard dice sicuramente "o" (vedi il link).
user187291

2
Sì, è esattamente vero: la precedenza è data alle stringhe rispetto ad altri tipi in concatenazione. Se uno degli operandi è una stringa, l'intera cosa verrà concatenata come stringa. Buona risposta.
devios1

76

Ecco qualche dettaglio in più, prima di arrivare alla risposta:

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 toStringfunzione non è "superata" valueOfin generale. Lo standard ECMAScript in realtà risponde abbastanza bene a questa domanda. Ogni oggetto ha una [[DefaultValue]]proprietà, che viene calcolata su richiesta. Quando si richiede questa proprietà, l'interprete fornisce anche un "suggerimento" per il tipo di valore che si aspetta. Se il suggerimento è String, toStringviene utilizzato prima valueOf. Ma, se il suggerimento è Number, valueOfverrà utilizzato per primo. Nota che se solo uno è presente, o restituisce un non primitivo, di solito chiamerà l'altro come seconda scelta.

L' +operatore fornisce sempre il suggerimento Number, anche se il primo operando è un valore stringa. Anche se richiede la xsua Numberrappresentazione, poiché il primo operando restituisce una stringa da [[DefaultValue]], esegue la concatenazione di stringhe.

Se vuoi garantire che toStringvenga chiamato per la concatenazione di stringhe, usa un array e il .join("")metodo.

+Tuttavia, ActionScript 3.0 modifica leggermente il comportamento di . Se uno degli operandi è a String, lo tratterà come un operatore di concatenazione di stringhe e utilizzerà il suggerimento Stringquando chiama [[DefaultValue]]. Quindi, in AS3, questo esempio restituisce "foo, x = foo, foo = x, foo1, 43, x = foo ".)


1
Notare inoltre che se valueOfo toStringrestituiscono non primitive, vengono ignorate. Se nessuno dei due esiste o nessuno dei due restituisce una primitiva, TypeErrorviene generato a.
bcherry

1
Grazie Bcherry, questo è il calibro della risposta che speravo. Ma non dovrebbe x + "x ="; rendimento "42x ="? E x + "1"; resa 421? Inoltre, hai un URL per la parte rilevante dello standard ECMAScript?
brainjam

2
In realtà, '+' non usa suggerimenti (vedere $ 11.6.1) quindi ToPrimitive invoca [[DefaultValue]](no-hint), che è equivalente a [[DefaultValue]](number).
user187291

9
Questo non sembra essere il caso della classe Date incorporata. ("" + new Date(0)) === new Date(0).toString(). Un oggetto Date sembra sempre restituire il suo toString()valore quando viene aggiunto a qualcosa.
kpozin

7
+1 e grazie! Ho trovato il tuo post sul blog in cui elabori questa risposta e volevo collegarlo / condividerlo qui. È stato davvero utile l'aggiunta a questa risposta (incluso il commento di Dmitry A. Soshnikov).
GitaarLAB

1

TLDR

La coercizione del tipo, o conversione del tipo implicita, consente una digitazione debole e viene utilizzata in JavaScript. La maggior parte degli operatori (con la notevole eccezione degli operatori di uguaglianza rigorosa ===e !==) e delle operazioni di controllo del valore (ad es. if(value)...), Forzeranno i valori forniti loro, se i tipi di quei valori non sono immediatamente compatibili con l'operazione.

Il meccanismo preciso utilizzato per forzare un valore dipende dall'espressione valutata. Nella domanda viene utilizzato l' operatore di addizione .

L'operatore di addizione assicurerà innanzitutto che entrambi gli operandi siano primitivi, il che, in questo caso, implica la chiamata del valueOfmetodo. Il toStringmetodo non viene chiamato in questa istanza perché il valueOfmetodo sottoposto a override sull'oggetto xrestituisce un valore primitivo.

Quindi, poiché uno degli operandi nella domanda è una stringa, entrambi gli operandi vengono convertiti in stringhe. Questo processo utilizza l'operazione astratta interna ToString(nota: maiuscola) ed è distinta dal toStringmetodo sull'oggetto (o dalla sua catena di prototipi).

Infine, le stringhe risultanti vengono concatenate.

Dettagli

Sul prototipo di ogni oggetto funzione del costruttore corrispondente a ogni tipo di linguaggio in JavaScript (ad esempio Number, BigInt, String, Boolean, Symbol e Object), ci sono due metodi: valueOfe toString.

Lo scopo di valueOfè recuperare il valore primitivo associato a un oggetto (se ne ha uno). Se un oggetto non ha un valore primitivo sottostante, l'oggetto viene semplicemente restituito.

Se valueOfviene invocato su una primitiva, la primitiva viene auto-boxed in modo normale e viene restituito il valore della primitiva sottostante. Notare che per le stringhe, il valore primitivo sottostante (cioè il valore restituito da valueOf) è la rappresentazione della stringa stessa.

Il codice seguente mostra che il valueOfmetodo restituisce il valore primitivo sottostante da un oggetto wrapper e mostra come le istanze di oggetti non modificate che non corrispondono alle primitive non hanno alcun valore primitivo da restituire, quindi restituiscono semplicemente se stesse.

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

Lo scopo di toString, d'altra parte, è restituire una rappresentazione di stringa di un oggetto.

Per esempio:

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

Per la maggior parte delle operazioni, JavaScript tenterà silenziosamente di convertire uno o più operandi nel tipo richiesto. Questo comportamento è stato scelto per rendere JavaScript più facile da usare. JavaScript inizialmente non aveva eccezioni e anche questo potrebbe aver giocato un ruolo in questa decisione di progettazione. Questo tipo di conversione del tipo implicito è chiamato coercizione del tipo ed è la base del sistema di tipi libero (debole) di JavaScript. Le complicate regole alla base di questo comportamento hanno lo scopo di spostare la complessità del typecasting nel linguaggio stesso e fuori dal codice.

Durante il processo coercitivo, ci sono due modalità di conversione che possono verificarsi:

  1. Conversione di un oggetto in una primitiva (che potrebbe implicare una conversione di tipo stessa) e
  2. Conversione diretta a un'istanza tipo specifico, utilizzando un oggetto funzione di costruzione di uno dei tipi primitivi (es. Number(), Boolean(), String()Etc.)

Conversione in un primitivo

Quando si tenta di convertire tipi non primitivi in ​​primitive su cui operare, l'operazione astratta ToPrimitiveviene chiamata con un "suggerimento" opzionale di "numero" o "stringa". Se il suggerimento viene omesso, il suggerimento predefinito è "numero" (a meno che il @@toPrimitivemetodo non sia stato sovrascritto). Se il suggerimento è "stringa", toStringviene provato prima e valueOfsecondo se toStringnon ha restituito una primitiva. Altrimenti, viceversa. Il suggerimento dipende dall'operazione che richiede la conversione.

L'operatore di aggiunta non fornisce alcun suggerimento, quindi valueOfviene provato per primo. L'operatore di sottrazione fornisce un accenno di "numero", quindi valueOfviene provato per primo. Le uniche situazioni che riesco a trovare nella specifica in cui il suggerimento è "stringa" sono:

  1. Object#toString
  2. L'operazione astratta ToPropertyKey, che converte un argomento in un valore che può essere utilizzato come chiave di proprietà

Conversione diretta del tipo

Ogni operatore ha le proprie regole per completare la propria operazione. L'operatore di addizione utilizzerà innanzitutto ToPrimitiveper garantire che ogni operando sia primitivo; quindi, se uno degli operandi è una stringa, richiamerà deliberatamente l'operazione astratta ToStringsu ogni operando, per fornire il comportamento di concatenazione delle stringhe che ci aspettiamo con le stringhe. Se, dopo il ToPrimitivepassaggio, entrambi gli operandi non sono stringhe, viene eseguita l'addizione aritmetica.

A differenza dell'addizione, l'operatore di sottrazione non ha un comportamento di sovraccarico, quindi invocherà toNumericsu ogni operando dopo averli convertiti prima in primitive usando ToPrimitive.

Così:

 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

Si noti che l' Dateoggetto intrinseco è unico, in quanto è l'unico intrinseco a sovrascrivere il @@toPrimitivemetodo predefinito , in cui si presume che il suggerimento predefinito sia "stringa" (anziché "numero"). Il motivo per avere questo, è che le Dateistanze traducono in stringhe leggibili per impostazione predefinita, invece del loro valore numerico, per comodità del programmatore. Puoi sovrascrivere i @@toPrimitivetuoi oggetti usando Symbol.toPrimitive.

La seguente griglia mostra i risultati della coercizione per l'operatore di uguaglianza astratto ( ==) ( fonte ):

inserisci qui la descrizione dell'immagine

Vedi anche .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.