TLDR
A coerção de tipo, ou conversão de tipo implícita, permite a digitação fraca e é usada em todo o JavaScript. A maioria dos operadores (com a notável exceção dos operadores de igualdade estrita ===e !==) e operações de verificação de valor (por exemplo if(value)...), forçará os valores fornecidos a eles, se os tipos desses valores não forem imediatamente compatíveis com a operação.
O mecanismo preciso usado para coagir um valor depende da expressão que está sendo avaliada. Na pergunta, o operador de adição está sendo usado.
O operador de adição primeiro garantirá que ambos os operandos sejam primitivos, o que, neste caso, envolve a chamada do valueOfmétodo. O toStringmétodo não é chamado nesta instância porque o valueOfmétodo sobrescrito no objeto xretorna um valor primitivo.
Então, como um dos operandos em questão é uma string, ambos os operandos são convertidos em strings. Esse processo usa a operação interna abstrata ToString(nota: letras maiúsculas) e é diferente do toStringmétodo no objeto (ou de sua cadeia de protótipo).
Finalmente, as strings resultantes são concatenadas.
Detalhes
No protótipo de cada objeto de função do construtor correspondente a cada tipo de linguagem em JavaScript (ou seja, Number, BigInt, String, Boolean, Symbol e Object), existem dois métodos: valueOfe toString.
O objetivo de valueOfé recuperar o valor primitivo associado a um objeto (se houver). Se um objeto não tiver um valor primitivo subjacente, o objeto será simplesmente retornado.
Se valueOffor invocado contra um primitivo, então o primitivo é automaticamente encaixotado da maneira normal e o valor primitivo subjacente é retornado. Observe que, para strings, o valor primitivo subjacente (ou seja, o valor retornado por valueOf) é a própria representação da string.
O código a seguir mostra que o valueOfmétodo retorna o valor primitivo subjacente de um objeto wrapper e mostra como as instâncias de objeto não modificadas que não correspondem aos primitivos não têm valor primitivo para retornar, portanto, simplesmente retornam a si mesmas.
console.log(typeof new Boolean(true)) // 'object'
console.log(typeof new Boolean(true).valueOf()) // 'boolean'
console.log(({}).valueOf()) // {} (no primitive value to return)
O objetivo de toString, por outro lado, é retornar uma representação em string de um objeto.
Por exemplo:
console.log({}.toString()) // '[object Object]'
console.log(new Number(1).toString()) // '1'
Para a maioria das operações, o JavaScript tentará silenciosamente converter um ou mais operando (s) para o tipo necessário. Esse comportamento foi escolhido para tornar o JavaScript mais fácil de usar. JavaScript inicialmente não tinha exceções e isso também pode ter desempenhado um papel nesta decisão de design. Esse tipo de conversão implícita de tipo é chamado de coerção de tipo e é a base do sistema de tipo flexível (fraco) do JavaScript. As regras complicadas por trás desse comportamento têm o objetivo de mover a complexidade da conversão de tipos para a própria linguagem e para fora do seu código.
Durante o processo coercitivo, existem dois modos de conversão que podem ocorrer:
- Conversão de um objeto em um primitivo (que pode envolver uma conversão de tipo em si), e
- Conversão direta para uma instância de tipo específico, usando um objeto de função de construtor de um dos tipos primitivos (ou seja
Number(), . Boolean(), String()Etc.)
Conversão para um primitivo
Ao tentar converter tipos não primitivos em primitivos a serem operados, a operação abstrata ToPrimitiveé chamada com uma "dica" opcional de 'número' ou 'string'. Se a dica for omitida, a dica padrão é 'número' (a menos que o @@toPrimitivemétodo tenha sido substituído). Se a dica for 'string', ela toStringserá tentada primeiro e , em seguida , valueOfse toStringnão retornar um primitivo. Caso contrário, vice-versa. A dica depende da operação que solicita a conversão.
O operador de adição não fornece nenhuma dica, então valueOfé tentado primeiro. O operador de subtração fornece uma dica de 'número', então valueOfé tentado primeiro. As únicas situações que posso encontrar na especificação em que a dica é 'string' são:
Object#toString
- A operação abstrata
ToPropertyKey, que converte um argumento em um valor que pode ser usado como uma chave de propriedade
Conversão direta de tipo
Cada operador tem suas próprias regras para completar sua operação. O operador de adição será usado primeiro ToPrimitivepara garantir que cada operando seja um primitivo; então, se qualquer operando for uma string, ele invocará deliberadamente a operação abstrata ToStringem cada operando, para entregar o comportamento de concatenação de string que esperamos com as strings. Se, após a ToPrimitiveetapa, ambos os operandos não forem strings, então a adição aritmética é executada.
Ao contrário da adição, o operador de subtração não tem comportamento sobrecarregado e, portanto, invocará toNumericcada operando tendo primeiro os convertido em primitivos usando ToPrimitive.
Assim:
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
Observe que o Dateobjeto intrínseco é único, pois é o único intrínseco a substituir o @@toPrimitivemétodo padrão , no qual a dica padrão é presumida como 'string' (em vez de 'número'). A razão para isso é que as Dateinstâncias sejam convertidas em strings legíveis por padrão, em vez de seu valor numérico, para a conveniência do programador. Você pode substituir @@toPrimitiveem seus próprios objetos usando Symbol.toPrimitive.
A grade a seguir mostra os resultados da coerção para o operador de igualdade abstrato ( ==) ( fonte ):

Veja também .
window.console.log (x);oualert (x);?