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. Сложные правила, лежащие в основе такого поведения, призваны перенести сложность приведения типов в сам язык и из вашего кода.
В процессе принуждения могут происходить два режима преобразования:
- Преобразование объекта в примитив (что может включать само преобразование типа) и
- Прямое преобразование к экземпляру типа специфический, используя функцию конструктора объект одного из примитивных типов (т.е..
Number(), Boolean(), И String()т.д.)
Преобразование в примитив
При попытке преобразовать непримитивные типы в примитивы, над которыми нужно работать, абстрактная операция ToPrimitiveвызывается с необязательной «подсказкой» типа «число» или «строка». Если подсказка опущена, подсказка по умолчанию - «число» (если @@toPrimitiveметод не был переопределен). Если подсказка - строка, то toStringсначала выполняется попытка, а затем , valueOfесли toStringпримитив не вернул. Иначе наоборот. Подсказка зависит от операции, запрашивающей преобразование.
Оператор сложения не подсказывает, поэтому valueOfсначала выполняется попытка. Оператор вычитания дает намек на «число», поэтому valueOfсначала выполняется попытка. Единственные ситуации, которые я могу найти в спецификации, в которых подсказка является «строкой»:
Object#toString
- Абстрактная операция
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.
В следующей таблице показаны результаты принуждения для абстрактного оператора равенства ( ==) ( источник ):

См. Также .
window.console.log (x);илиalert (x);?