O que acontece quando objetos são adicionados obj1 + obj2
, subtraídos obj1 - obj2
ou impressos usando alert(obj)
?
JavaScript não permite personalizar como os operadores funcionam nos objetos. Ao contrário de algumas outras linguagens de programação, como Ruby ou C++, não podemos implementar um método de objeto especial para lidar com adição (ou outros operadores).
No caso de tais operações, os objetos são convertidos automaticamente em primitivos e, em seguida, a operação é realizada sobre esses primitivos e resulta em um valor primitivo.
Essa é uma limitação importante: o resultado de obj1 + obj2
(ou outra operação matemática) não pode ser outro objeto!
Por exemplo, não podemos fazer objetos representando vetores ou matrizes (ou conquistas ou qualquer outra coisa), adicioná-los e esperar um objeto “somado” como resultado. Tais feitos arquitetônicos estão automaticamente “fora de questão”.
Então, como não podemos fazer muita coisa tecnicamente aqui, não há matemática com objetos em projetos reais. Quando isso acontece, com raras exceções, é devido a um erro de codificação.
Neste capítulo, abordaremos como um objeto é convertido em primitivo e como personalizá-lo.
Temos dois propósitos:
Date
). Iremos encontrá-los mais tarde.No capítulo Conversões de Tipo vimos as regras para conversões numéricas, de strings e booleanas de primitivas. Mas deixamos uma lacuna para os objetos. Agora, como conhecemos métodos e símbolos, torna-se possível preenchê-lo.
true
em um contexto booleano, simples assim. Existem apenas conversões numéricas e de strings.Date
(a serem abordados no capítulo Data e hora) podem ser subtraídos, e o resultado de date1 - date2
é a diferença horária entre duas datas.alert(obj)
e em contextos semelhantes.Podemos implementar a conversão de strings e números por nós mesmos, usando métodos de objetos especiais.
Agora vamos entrar em detalhes técnicos, pois é a única maneira de abordar o tema em profundidade.
Como o JavaScript decide qual conversão aplicar?
Existem três variantes de conversão de tipo, que acontecem em diversas situações. Eles são chamados de “dicas”, conforme descrito na especificação:
"string"
Para uma conversão de objeto em string, quando estamos realizando uma operação em um objeto que espera uma string, como alert
:
// output alert(obj); // using object as a property key anotherObj[obj] = 123;
"number"
Para uma conversão de objeto em número, como quando fazemos contas:
// explicit conversion let num = Number(obj); // maths (except binary plus) let n = +obj; // unary plus let delta = date1 - date2; // less/greater comparison let greater = user1 > user2;
A maioria das funções matemáticas integradas também inclui essa conversão.
"default"
Ocorre em casos raros quando o operador “não tem certeza” de que tipo esperar.
Por exemplo, binário plus +
pode funcionar tanto com strings (concatena-as) quanto com números (adiciona-as). Portanto, se um plus binário obtiver um objeto como argumento, ele usará a dica "default"
para convertê-lo.
Além disso, se um objeto for comparado usando ==
com uma string, número ou símbolo, também não está claro qual conversão deve ser feita, então a dica "default"
é usada.
// binary plus uses the "default" hint let total = obj1 + obj2; // obj == number uses the "default" hint if (user == 1) { ... };
Os operadores de comparação maior e menor, como <
>
, também podem funcionar com strings e números. Ainda assim, eles usam a dica "number"
e não "default"
. Isso é por razões históricas.
Na prática, porém, as coisas são um pouco mais simples.
Todos os objetos integrados, exceto um caso (objeto Date
, aprenderemos mais tarde) implementam a conversão "default"
da mesma forma que "number"
. E provavelmente deveríamos fazer o mesmo.
Ainda assim, é importante conhecer todas as 3 dicas, em breve veremos o porquê.
Para fazer a conversão, o JavaScript tenta encontrar e chamar três métodos de objeto:
obj[Symbol.toPrimitive](hint)
– o método com a chave simbólica Symbol.toPrimitive
(símbolo do sistema), se tal método existir,"string"
obj.toString()
ou obj.valueOf()
, o que existir."number"
ou "default"
obj.valueOf()
ou obj.toString()
, o que existir. Vamos começar pelo primeiro método. Há um símbolo integrado chamado Symbol.toPrimitive
que deve ser usado para nomear o método de conversão, assim:
obj[Symbol.toPrimitive] = function(hint) { // here goes the code to convert this object to a primitive // it must return a primitive value // hint = one of "string", "number", "default" };
Se o método Symbol.toPrimitive
existir, ele será usado para todas as dicas e não serão necessários mais métodos.
Por exemplo, aqui o objeto user
o implementa:
let user = { name: "John", money: 1000, [Symbol.toPrimitive](hint) { alert(`hint: ${hint}`); return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; // conversions demo: alert(user); // hint: string -> {name: "John"} alert(+user); // hint: number -> 1000 alert(user + 500); // hint: default -> 1500
Como podemos ver no código, user
se torna uma string autodescritiva ou uma quantia em dinheiro, dependendo da conversão. O método único user[Symbol.toPrimitive]
lida com todos os casos de conversão.
Se não houver Symbol.toPrimitive
então o JavaScript tenta encontrar os métodos toString
e valueOf
:
"string"
: chame o método toString
e, se ele não existir ou retornar um objeto em vez de um valor primitivo, chame valueOf
(para que toString
tenha prioridade para conversões de string).valueOf
e, se ele não existir ou retornar um objeto em vez de um valor primitivo, chame toString
(para que valueOf
tenha prioridade para matemática). Os métodos toString
e valueOf
vêm desde os tempos antigos. Eles não são símbolos (os símbolos não existiam há muito tempo), mas sim métodos “regulares” nomeados por strings. Eles fornecem uma maneira alternativa ao “estilo antigo” de implementar a conversão.
Esses métodos devem retornar um valor primitivo. Se toString
ou valueOf
retornar um objeto, ele será ignorado (o mesmo que se não houvesse método).
Por padrão, um objeto simples possui os seguintes métodos toString
e valueOf
:
toString
retorna uma string "[object Object]"
.valueOf
retorna o próprio objeto.Aqui está a demonstração:
let user = {name: "John"}; alert(user); // [object Object] alert(user.valueOf() === user); // true
Portanto, se tentarmos usar um objeto como uma string, como em um alert
ou algo assim, por padrão veremos [object Object]
.
O valueOf
padrão é mencionado aqui apenas para fins de integridade, para evitar qualquer confusão. Como você pode ver, ele retorna o próprio objeto e, portanto, é ignorado. Não me pergunte por quê, isso é por razões históricas. Portanto, podemos assumir que não existe.
Vamos implementar esses métodos para personalizar a conversão.
Por exemplo, aqui user
faz o mesmo acima usando uma combinação de toString
e valueOf
em vez de Symbol.toPrimitive
:
let user = { name: "John", money: 1000, // for hint="string" toString() { return `{name: "${this.name}"}`; }, // for hint="number" or "default" valueOf() { return this.money; } }; alert(user); // toString -> {name: "John"} alert(+user); // valueOf -> 1000 alert(user + 500); // valueOf -> 1500
Como podemos ver, o comportamento é o mesmo do exemplo anterior com Symbol.toPrimitive
.
Freqüentemente, queremos um único local “pega-tudo” para lidar com todas as conversões primitivas. Neste caso, podemos implementar apenas toString
, assim:
let user = { name: "John", toString() { return this.name; } }; alert(user); // toString -> John alert(user + 500); // toString -> John500
Na ausência de Symbol.toPrimitive
e valueOf
, toString
tratará de todas as conversões primitivas.
O importante a saber sobre todos os métodos de conversão de primitivos é que eles não retornam necessariamente o primitivo “sugerido”.
Não há controle se toString
retorna exatamente uma string ou se o método Symbol.toPrimitive
retorna um número para a dica "number"
.
A única coisa obrigatória: esses métodos devem retornar um primitivo, não um objeto.
Por razões históricas, se toString
ou valueOf
retornar um objeto, não há erro, mas tal valor é ignorado (como se o método não existisse). Isso porque nos tempos antigos não existia um bom conceito de “erro” em JavaScript.
Em contrapartida, Symbol.toPrimitive
é mais rigoroso, deve retornar um primitivo, caso contrário haverá um erro.
Como já sabemos, muitos operadores e funções realizam conversões de tipo, por exemplo, multiplicação *
converte operandos em números.
Se passarmos um objeto como argumento, existem duas etapas de cálculo:
Por exemplo:
let obj = { // toString handles all conversions in the absence of other methods toString() { return "2"; } }; alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number
obj * 2
primeiro converte o objeto em primitivo (que é uma string "2"
)."2" * 2
se torna 2 * 2
(a string é convertida em número).Binary plus irá concatenar strings na mesma situação, pois aceita uma string com prazer:
let obj = { toString() { return "2"; } }; alert(obj + 2); // "22" ("2" + 2), conversion to primitive returned a string => concatenation
A conversão de objeto em primitivo é chamada automaticamente por muitas funções e operadores integrados que esperam um primitivo como valor.
Existem 3 tipos (dicas):
"string"
(para alert
e outras operações que precisam de uma string)"number"
(para matemática)"default"
(poucos operadores, geralmente objetos, implementam-no da mesma forma que "number"
)A especificação descreve explicitamente qual operador usa qual dica.
O algoritmo de conversão é:
obj[Symbol.toPrimitive](hint)
se o método existir,"string"
obj.toString()
ou obj.valueOf()
, o que existir."number"
ou "default"
obj.valueOf()
ou obj.toString()
, o que existir.Todos esses métodos devem retornar um primitivo para funcionar (se definido).
Na prática, muitas vezes é suficiente implementar apenas obj.toString()
como um método “pega-tudo” para conversões de strings que devem retornar uma representação “legível por humanos” de um objeto, para fins de registro ou depuração.