valueOf () a toString () w JavaScript


115

W Javascript każdy obiekt ma metodę valueOf () i toString (). Pomyślałbym, że metoda toString () została wywołana za każdym razem, gdy wywoływana jest konwersja ciągów, ale najwyraźniej jest ona przejmowana przez valueOf ().

Na przykład kod

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

wydrukuje

x=42
x=foo

Wydaje mi się, że jest to odwrócone ... gdyby x było na przykład liczbą zespoloną, chciałbym, aby wartość valueOf () podała mi swoją wielkość, ale kiedy chcę zamienić na ciąg, chciałbym coś w rodzaju „a + bi”. I nie chciałbym jawnie wywoływać metody toString () w kontekstach, które implikują łańcuch.

Czy tak po prostu jest?


6
Czy próbowałeś window.console.log (x);lub alert (x);?
Li0liQ

5
Dają odpowiednio „Object” i „foo”. Zabawne rzeczy.
brainjam

Właściwie alert (x); daje "foo" i window.console.log (x); daje "foo {}" w Firebug i cały obiekt w konsoli Chrome.
brainjam

W przeglądarce Firefox 33.0.2 alert(x)wyświetla fooi window.console.log(x)wyświetla Object { toString: x.toString(), valueOf: x.valueOf() }.
John Sonderson

Odpowiedzi:


107

Powód, dla którego („x =” + x) daje „x = wartość”, a nie „x = tostring”, jest następujący. Oceniając „+”, javascript najpierw zbiera pierwotne wartości operandów, a następnie decyduje, czy należy zastosować dodawanie, czy konkatenację, na podstawie typu każdego prymitywu.

Tak więc myślisz, że to działa

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

i tak właśnie się dzieje

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

Oznacza to, że toString jest stosowane do wyniku valueOf, a nie do oryginalnego obiektu.

Więcej informacji można znaleźć w sekcji 11.6.1 Operator dodawania (+) w specyfikacji języka ECMAScript.


* Po wywołaniu w kontekście strun ToPrimitive robi invoke toString, ale to nie jest w tym przypadku, ponieważ „+” nie narzuca żadnego kontekstu typu.


3
Czy warunek w „faktycznie” bloku nie powinien być przeczytany, jeśli (pa to string && pb to string) "? Tj. „&&” zamiast „||” ?
brainjam

3
Standard zdecydowanie mówi „lub” (patrz link).
user187291

2
Tak, zgadza się - w konkatenacji łańcuchy mają pierwszeństwo przed innymi typami. Jeśli którykolwiek z operandów jest łańcuchem, całość zostanie połączona jako łańcuch. Dobra odpowiedź.
devios1

76

Oto trochę więcej szczegółów, zanim przejdę do odpowiedzi:

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"

Ogólnie rzecz biorąc, toStringfunkcja nie jest „prześcigana” valueOf. Standard ECMAScript właściwie odpowiada na to pytanie całkiem dobrze. Każdy obiekt ma [[DefaultValue]]właściwość, która jest obliczana na żądanie. Pytając o tę właściwość, interpreter podaje również „wskazówkę”, jakiego rodzaju wartości oczekuje. Jeśli podpowiedź jest String, to toStringjest używane wcześniej valueOf. Ale jeśli podpowiedź jest taka Number, valueOfzostanie użyta pierwsza. Zauważ, że jeśli tylko jeden jest obecny lub zwraca nieprymitywny, zwykle wywołuje drugi wybór.

+Operator zawsze daje wskazówkę Number, nawet jeśli pierwszy argument jest ciąg znaków. Mimo że prosi xo Numberreprezentację, ponieważ pierwszy operand zwraca ciąg z [[DefaultValue]], wykonuje konkatenację ciągów.

Jeśli chcesz zagwarantować, że toStringjest to wywoływane do konkatenacji ciągów, użyj tablicy i .join("")metody.

( +Jednak ActionScript 3.0 nieznacznie modyfikuje zachowanie programu . Jeśli którykolwiek z operandów to a String, potraktuje go jako operator konkatenacji ciągów i użyje wskazówki Stringpodczas wywołania [[DefaultValue]]. Tak więc w AS3 ten przykład daje "foo, x = foo, foo = x, foo1, 43, x = foo ".)


1
Zauważ również, że jeśli valueOflub toStringzwrócą inne niż prymitywy, są one ignorowane. Jeśli żaden nie istnieje lub żaden nie zwraca prymitywu, TypeErrorwyrzucany jest a .
bcherry

1
Dzięki bcherry, to jest kaliber odpowiedzi, na którą liczyłem. Ale nie powinno x + "x ="; wydajność "42x ="? I x + "1"; wydajność 421? Czy masz również adres URL odpowiedniej części standardu ECMAScript?
brainjam

2
Właściwie '+' nie używa podpowiedzi (zobacz $ 11.6.1), dlatego wywołuje ToPrimitive [[DefaultValue]](no-hint), co jest równoważne [[DefaultValue]](number).
user187291

9
Wydaje się, że nie ma to miejsca w przypadku wbudowanej klasy Date. ("" + new Date(0)) === new Date(0).toString(). Obiekt Date zawsze wydaje się zwracać swoją toString()wartość, gdy jest do czegoś dodany.
kpozin

7
+1 i dziękuję! Znalazłem twój post na blogu, w którym rozwinąłeś tę odpowiedź i chciałem ją tutaj zamieścić / udostępnić. Było to bardzo pomocne uzupełnienie tej odpowiedzi (łącznie z komentarzem Dmitrija A. Sosznikowa).
GitaarLAB

1

TLDR

Wymuszenie typu lub niejawna konwersja typów umożliwia słabe pisanie i jest używane w całym JavaScript. Większość operatorów (z godnym uwagi wyjątkiem operatorów ścisłej równości ===i !==) oraz operacji sprawdzania wartości (np. if(value)...) Wymusi wartości dostarczone do nich, jeśli typy tych wartości nie są bezpośrednio zgodne z operacją.

Dokładny mechanizm używany do wymuszania wartości zależy od ocenianego wyrażenia. W pytaniu używany jest operator dodawania .

Operator dodawania najpierw upewni się, że oba operandy są prymitywami, co w tym przypadku obejmuje wywołanie valueOfmetody. toStringMetoda ta nazywa się w tym przypadku, ponieważ zastąpiona valueOfmetoda na obiekcie xzwraca pierwotną wartość.

Następnie, ponieważ jeden z argumentów w pytaniu jest łańcuchem, oba operandy są konwertowane na łańcuchy. Ten proces wykorzystuje abstrakcyjną, wewnętrzną operację ToString(uwaga: pisane wielką literą) i różni się od toStringmetody na obiekcie (lub łańcuchu prototypów).

Na koniec powstałe ciągi są konkatenowane.

Detale

W prototypie każdego obiektu funkcji konstruktora odpowiadającego każdemu typowi języka w JavaScript (tj. Number, BigInt, String, Boolean, Symbol i Object) istnieją dwie metody: valueOfi toString.

Celem valueOfjest pobranie wartości pierwotnej skojarzonej z obiektem (jeśli taki posiada). Jeśli obiekt nie ma podstawowej wartości pierwotnej, to obiekt jest po prostu zwracany.

Jeśli valueOfjest wywoływana w odniesieniu do prymitywu, wówczas prymityw jest w normalny sposób zamykany automatycznie i zwracana jest podstawowa wartość pierwotna. Zauważ, że w przypadku łańcuchów podstawową wartością pierwotną (tj. Wartością zwracaną przez valueOf) jest sama reprezentacja ciągu.

Poniższy kod pokazuje, że valueOfmetoda zwraca podstawową wartość pierwotną z obiektu otoki i pokazuje, jak niezmodyfikowane instancje obiektów, które nie odpowiadają elementom pierwotnym, nie mają wartości pierwotnej do zwrócenia, więc po prostu zwracają się same.

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

Z toStringdrugiej strony, celem jest zwrócenie ciągu reprezentującego obiekt.

Na przykład:

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

W przypadku większości operacji JavaScript próbuje po cichu przekonwertować jeden lub więcej operandów na wymagany typ. To zachowanie zostało wybrane, aby ułatwić korzystanie z JavaScript. JavaScript początkowo nie miał wyjątków i mogło to również odegrać rolę w tej decyzji projektowej. Ten rodzaj niejawnej konwersji typu nazywany jest wymuszeniem typu i stanowi podstawę luźnego (słabego) systemu typu JavaScript. Złożone reguły stojące za tym zachowaniem mają na celu przeniesienie złożoności rzutowania typów do samego języka i poza kod.

Podczas procesu przymusu mogą wystąpić dwa tryby konwersji:

  1. Konwersja obiektu na element pierwotny (który może obejmować samą konwersję typów) i
  2. Bezpośrednia konwersja do instancji specyficzny typ, za pomocą obiektu Funkcja konstruktora jednego z typów pierwotnych (tj. Number(), Boolean(), String()Itd.)

Nawrócenie na prymityw

Przy próbie konwersji typów innych niż pierwotne na prymitywy, na których mają być wykonywane operacje, ToPrimitivewywoływana jest operacja abstrakcyjna z opcjonalną wskazówką „liczba” lub „ciąg znaków”. Jeśli pominięto wskazówkę, domyślną wskazówką jest „liczba” (chyba że @@toPrimitivemetoda została nadpisana). Jeśli wskazówką jest „łańcuch”, toStringto najpierw jest próbowana, a valueOfdruga, jeśli toStringnie zwróciła prymitywu. W przeciwnym razie odwrotnie. Wskazówka zależy od operacji żądającej konwersji.

Operator dodawania nie dostarcza żadnej wskazówki, więc valueOfjest najpierw wypróbowywany. Operator odejmowania podaje wskazówkę dotyczącą „liczby”, więc valueOfjest próbowana jako pierwsza. Jedyne sytuacje, które mogę znaleźć w specyfikacji, w których wskazówką jest „ciąg znaków”, to:

  1. Object#toString
  2. Operacja abstrakcyjna ToPropertyKey, która konwertuje argument na wartość, która może być używana jako klucz właściwości

Bezpośrednia konwersja typu

Każdy operator ma własne zasady wykonywania operacji. Operator dodawania najpierw użyje, ToPrimitiveaby upewnić się, że każdy operand jest prymitywem; wtedy, jeśli którykolwiek z operandów jest łańcuchem, celowo wywoła operację abstrakcyjną ToStringna każdym operandzie, aby zapewnić zachowanie konkatenacji łańcuchów, którego oczekujemy z łańcuchami. Jeśli po ToPrimitivekroku oba operandy nie są łańcuchami, wykonywane jest dodawanie arytmetyczne.

W przeciwieństwie do dodawania, operator odejmowania nie ma przeciążonego zachowania, więc wywoła toNumerickażdy operand po uprzednim przekonwertowaniu go na prymitywy przy użyciu ToPrimitive.

Więc:

 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

Należy zauważyć, że Dateobiekt wewnętrzny jest unikalny, ponieważ jest jedyną wewnętrzną @@toPrimitivemetodą zastępującą domyślną metodę, w której domyślną wskazówką jest „ciąg” (a nie „liczba”). Powodem tego jest to, że Dateinstancje są domyślnie tłumaczone na czytelne ciągi zamiast ich wartości liczbowych, dla wygody programisty. Możesz nadpisać @@toPrimitiveswoje własne obiekty za pomocą Symbol.toPrimitive.

Poniższa tabela przedstawia wyniki wymuszenia dla abstrakcyjnego operatora równości ( ==) ( źródło ):

wprowadź opis obrazu tutaj

Zobacz także .

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.