valueOf () против toString () в Javascript

115

В Javascript каждый объект имеет методы valueOf () и toString (). Я бы подумал, что метод toString () вызывается всякий раз, когда вызывается преобразование строки, но, очевидно, он превосходит valueOf ().

Например, код

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

напечатает

x=42
x=foo

Это кажется мне обратным ... например, если бы x было комплексным числом, я бы хотел, чтобы valueOf () давал мне его величину, но всякий раз, когда я хотел преобразовать в строку, я хотел бы что-то вроде «a + bi». И я бы не хотел вызывать toString () явно в контекстах, подразумевающих строку.

Это просто так?

brainjam
источник
6
Вы пробовали window.console.log (x);или alert (x);?
Li0liQ
5
Они дают «Object» и «foo» соответственно. Веселая штука.
brainjam
Собственно, alert (x); дает "foo" и window.console.log (x); дает "foo {}" в Firebug и весь объект в консоли Chrome.
brainjam
В Firefox 33.0.2 alert(x)отображается fooи window.console.log(x)отображается Object { toString: x.toString(), valueOf: x.valueOf() }.
Джон Сондерсон

Ответы:

107

Причина, по которой ("x =" + x) дает "x = значение", а не "x = tostring", заключается в следующем. При оценке «+» javascript сначала собирает примитивные значения операндов, а затем решает, следует ли применять сложение или конкатенацию, в зависимости от типа каждого примитива.

Итак, как вы думаете, это работает

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

и вот что происходит на самом деле

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

То есть toString применяется к результату valueOf, а не к вашему исходному объекту.

Дополнительные сведения см. В разделе 11.6.1 Оператор сложения (+) в спецификации языка ECMAScript.


* При вызове в контексте строки, ToPrimitive делает Invoke ToString, но это не тот случай, потому что «+» не навязывает какой - либо контекст типа.

user187291
источник
3
Разве условное выражение в блоке «фактически» не следует читать «if (pa - строка && pb - строка)»? Т.е. "&&" вместо "||" ?
brainjam
3
В стандарте однозначно написано «или» (см. Ссылку).
user187291
2
Да, именно так - при конкатенации строкам отдается приоритет над другими типами. Если любой из операндов является строкой, все будет объединено в строку. Хороший ответ.
devios1
76

Прежде чем я перейду к ответу, расскажу немного подробнее:

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"

toStringФункция не «переиграла» на valueOfв целом. Стандарт ECMAScript действительно хорошо отвечает на этот вопрос. У каждого объекта есть [[DefaultValue]]свойство, которое вычисляется по запросу. При запросе этого свойства интерпретатор также дает «подсказку» о том, какое значение он ожидает. Если подсказка есть String, то toStringиспользуется раньше valueOf. Но, если подсказка есть Number, то valueOfсначала будет использовано. Обратите внимание, что если присутствует только один или он возвращает не примитив, он обычно вызывает другой как второй вариант.

+Оператор всегда дает подсказку Number, даже если первый операнд является строкой. Несмотря на то, что он запрашивает xего Numberпредставление, поскольку первый операнд возвращает строку из [[DefaultValue]], он выполняет конкатенацию строк.

Если вы хотите гарантировать, что toStringвызывается конкатенация строк, используйте массив и .join("")метод.

(ActionScript 3.0 +, однако, немного изменяет поведение . Если любой из операндов является a String, он будет рассматривать его как оператор конкатенации строк и использовать подсказку Stringпри вызове [[DefaultValue]]. Таким образом, в AS3 этот пример дает "foo, x = foo, foo = x, foo1, 43, x = foo ".)

bcherry
источник
1
Также обратите внимание, что if valueOfor toStringвозвращает непримитивы, они игнорируются. Если ни один из них не существует или ни один из них не возвращает примитив, то создается a TypeError.
bcherry
1
Спасибо, bcherry, это именно тот ответ, на который я надеялся. Но не должно x + "x ="; yield "42x ="? И х + «1»; выход 421? Кроме того, у вас есть URL-адрес соответствующей части стандарта ECMAScript?
brainjam
2
На самом деле '+' не использует подсказки (см. $ 11.6.1), поэтому вызывает ToPrimitive [[DefaultValue]](no-hint), что эквивалентно [[DefaultValue]](number).
user187291
9
Похоже, что это не относится к встроенному классу Date. ("" + new Date(0)) === new Date(0).toString(), Кажется, что объект Date всегда возвращает свое toString()значение, когда он к чему-то добавляется.
kpozin
7
+1 и спасибо! Я нашел ваше сообщение в блоге, в котором вы подробно рассказываете об этом ответе, и хотел связать его / поделиться здесь. Это было действительно полезное дополнение к этому ответу (включая комментарий Дмитрия Сошникова).
GitaarLAB
1

TLDR

Приведение типов, или неявное преобразование типов, обеспечивает слабую типизацию и используется во всем JavaScript. Большинство операторов (за заметным исключением операторов строгого равенства ===и !==) и операций проверки значений (например, if(value)...) будут приводить значения, предоставленные им, если типы этих значений не сразу совместимы с операцией.

Точный механизм, используемый для приведения значения, зависит от оцениваемого выражения. В вопросе используется оператор сложения .

Оператор сложения сначала гарантирует, что оба операнда являются примитивами, что в данном случае включает вызов valueOfметода. В toStringэтом экземпляре метод не вызывается, так как переопределенный valueOfметод объекта xвозвращает примитивное значение.

Затем, поскольку один из операндов в вопросе является строкой, оба операнда преобразуются в строки. Этот процесс использует абстрактную внутреннюю операцию ToString(примечание: с заглавной буквы) и отличается от toStringметода объекта (или его цепочки прототипов).

Наконец, результирующие строки объединяются.

подробности

В прототипе каждого объекта функции конструктора, соответствующего каждому типу языка в JavaScript (например, Number, BigInt, String, Boolean, Symbol и Object), есть два метода: valueOfи toString.

Цель valueOf- получить примитивное значение, связанное с объектом (если он есть). Если объект не имеет базового примитивного значения, то объект просто возвращается.

Если valueOfвызывается для примитива, то примитив автоматически упаковывается в коробку обычным способом, и возвращается базовое значение примитива. Обратите внимание, что для строк базовое примитивное значение (т. Е. Значение, возвращаемое valueOf) является самим строковым представлением.

Следующий код показывает, что valueOfметод возвращает базовое примитивное значение из объекта-оболочки, и показывает, как неизмененные экземпляры объекта, которые не соответствуют примитивам, не имеют примитивного значения для возврата, поэтому они просто возвращают себя.

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

С toStringдругой стороны, цель - вернуть строковое представление объекта.

Например:

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

Для большинства операций JavaScript будет молча попытаться преобразовать один или несколько операндов в требуемый тип. Это поведение было выбрано, чтобы упростить использование JavaScript. Изначально в JavaScript не было исключений , и это тоже могло сыграть роль в этом дизайнерском решении. Такой вид неявного преобразования типов называется приведением типов и является основой свободной (слабой) системы типов JavaScript. Сложные правила, лежащие в основе такого поведения, призваны перенести сложность приведения типов в сам язык и из вашего кода.

В процессе принуждения могут происходить два режима преобразования:

  1. Преобразование объекта в примитив (что может включать само преобразование типа) и
  2. Прямое преобразование к экземпляру типа специфический, используя функцию конструктора объект одного из примитивных типов (т.е.. Number(), Boolean(), И String()т.д.)

Преобразование в примитив

При попытке преобразовать непримитивные типы в примитивы, над которыми нужно работать, абстрактная операция ToPrimitiveвызывается с необязательной «подсказкой» типа «число» или «строка». Если подсказка опущена, подсказка по умолчанию - «число» (если @@toPrimitiveметод не был переопределен). Если подсказка - строка, то toStringсначала выполняется попытка, а затем , valueOfесли toStringпримитив не вернул. Иначе наоборот. Подсказка зависит от операции, запрашивающей преобразование.

Оператор сложения не подсказывает, поэтому valueOfсначала выполняется попытка. Оператор вычитания дает намек на «число», поэтому valueOfсначала выполняется попытка. Единственные ситуации, которые я могу найти в спецификации, в которых подсказка является «строкой»:

  1. Object#toString
  2. Абстрактная операция ToPropertyKey, которая преобразует аргумент в значение, которое может использоваться как ключ свойства.

Прямое преобразование типа

У каждого оператора есть свои правила выполнения своей операции. Сначала будет использоваться оператор сложения, ToPrimitiveчтобы убедиться, что каждый операнд является примитивом; затем, если один из операндов является строкой, он намеренно вызывает абстрактную операцию ToStringдля каждого операнда, чтобы обеспечить поведение конкатенации строк, которое мы ожидаем со строками. Если после ToPrimitiveшага оба операнда не являются строками, выполняется арифметическое сложение.

В отличие от сложения, оператор вычитания не имеет перегруженного поведения, поэтому он будет вызываться toNumericдля каждого операнда, предварительно преобразовав их в примитивы с помощью ToPrimitive.

Так:

 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

Обратите внимание, что Dateвнутренний объект уникален в том смысле, что это единственное внутреннее свойство, которое переопределяет @@toPrimitiveметод по умолчанию , в котором подсказка по умолчанию считается «строкой» (а не «числом»). Причина в том Date, что для удобства программиста экземпляры по умолчанию переводятся в читаемые строки, а не в их числовые значения. Вы можете переопределить @@toPrimitiveсвои собственные объекты, используя Symbol.toPrimitive.

В следующей таблице показаны результаты принуждения для абстрактного оператора равенства ( ==) ( источник ):

введите описание изображения здесь

См. Также .

Бен Астон
источник