Explicando a coerção de tipos em Javascript pt. 2
Coerção de tipos para objetos
Até agora, analisamos a coerção de tipos para valores primitivos. Isso não é muito empolgante.
Quando isso ocorre com objetos, e a engine encontra expressões como [1] + [2,3]
, primeiramente será preciso converter o objeto para um valor primitivo, que é então convertido pro tipo final. E ainda assim existem apenas três tipos de conversão: numérico, string e booleano.
O caso mais simples é a conversão para booleano: qualquer valor não primitivo sempre será convertido para true
, não importa se um objeto ou array está vazio ou não.
Objetos são convertidos para primitivos através da função [[ToPrimitive]]
, que é responsável pela conversão numérica e string.
Abaixo uma pseudo implementação do método [[ToPrimitive]]
:
function ToPrimitive(input, preferredType){
switch (preferredType){
case Number:
return toNumber(input);
break;
case String:
return toString(input);
break
default:
return toNumber(input);
}
function isPrimitive(value){
return value !== Object(value);
}
function toString(){
if (isPrimitive(input.toString())) return input.toString();
if (isPrimitive(input.valueOf())) return input.valueOf();
throw new TypeError();
}
function toNumber(){
if (isPrimitive(input.valueOf())) return input.valueOf();
if (isPrimitive(input.toString())) return input.toString();
throw new TypeError();
}
}
[[ToPrimitive]]
é invocado passando dois argumentos:
- input: valor a ser convertido;
- preferredType: Tipo escolhido para conversão, podendo ser
Number
ouString
. Esse argumento é opcional.
Ambas conversões, número e string fazem uso de dois métodos do objeto de entrada (input): valueOf
e toString
. Ambas funções são declaradas no Object.prototype
, portanto, disponível para qualquer tipo derivado, como Date
, Array
, e etc.
Em geral, o algoritmo é o seguinte:
-
Se o input já é do tipo primitivo, retorne-o;
-
Chame a função
input.toString()
, se o resultado for do tipo primitivo, retorne-o; -
Chame a função
input.valueOf()
, se o resultado for do tipo primitivo, retorne-o; -
Se nem a função
input.toString()
ouinput.valueOf()
retornar um tipo primitivo, lanceTypeError
.
Conversões numéricas primeiro chamam a função valueOf
(3) com o fallback toString
(2).
A conversão de string faz exatamente o oposto: toString
(2) seguido de valueOf
(3).
A maioria dos tipos internos(built-in) não possui a função valueOf
, ou possui valueOf
retornando o próprio objeto, então é ignorado por não ser do tipo primitivo. É por isso que a conversão de tipos number
e string
podem funcionar da mesma forma — ambos acabam chamando toString()
.
Operadores diferentes podem acionar a conversão numérica ou de string com a ajuda do parâmetro preferredType
. Mas existem duas exceções: o comparador de igualdade abstrato ==
e a opção binária +
acionam modos de conversão padrão (preferredType
não é especificado, ou igual a default). Nesse caso, a maior dos tipos internos(built-in) assumirão uma conversão numérica como default, exceto Date
que fará uma conversão de string.
Segue abaixo um exemplo de como se comporta uma conversa de Date
:
let d = new Date();
// obtém a representação em string
let str = d.toString(); // 'Wed Jan 17 2018 16:15:42'
// obtém a representação numérica, número em milisegundos desde a época do Unix
let num = d.valueOf(); // 1516198542525
// comparara com uma representação de string
// é true, pois "d" foi convertido para a mesma string
console.log(d == str); // true
// compara com a representação numérica
// false, pois d não foi convertido para um número usando valueOf()
console.log(d == num); // false
// O resulado é 'Wed Jan 17 2018 16:15:42Wed Jan 17 2018 16:15:42'
// '+' funcional igual ao '==', aciona o modo padrão de conversão
console.log(d + d);
// O resultado é 0, pois o operador '-' aciona explicitamente a conversão numérica, não a padrão
console.log(d - d);
Você pode sobrescrever os métodos padrão toString()
e valueOf()
para conectar-se à lógica de conversão objeto para primitivo(object-to-primitive).
var obj = {
prop: 101,
toString(){
return 'Prop: ' + this.prop;
},
valueOf() {
return this.prop;
}
};
console.log(String(obj)); // 'Prop: 101'
console.log(obj + '') // '101'
console.log(+obj); // 101
console.log(obj > 100); // true
Observe como obj + ‘’
retorna '101'
como uma string. O operador +
dispara um modo de conversão padrão, e como dito anteriormente, Object assume a conversão numérico como padrão, usando portanto, o método valueOf()
ao invés do toString()
.
Método do ES6 - Symbol.toPrimitive
No ES5 você pode conectar a lógica de conversão de objeto a primitivo(object-to-primitive) substituindo os métodos toString
e valueOf
.
No ES6 você pode ir mais longe, podendo substituir completamente a rotina interna [[ToPrimitive]]
implementando o método [Symbol.toPrimtive]
em um objeto.
class Disk {
constructor(capacity){
this.capacity = capacity;
}
[Symbol.toPrimitive](hint){
switch (hint) {
case 'string':
return 'Capacity: ' + this.capacity + ' bytes';
case 'number':
// convert to KiB
return this.capacity / 1024;
default:
// assume numeric conversion as a default
return this.capacity / 1024;
}
}
}
// 1MiB disk
let disk = new Disk(1024 * 1024);
console.log(String(disk)) // Capacity: 1048576 bytes
console.log(disk + '') // '1024'
console.log(+disk); // 1024
console.log(disk > 1000); // true
Exemplos
Sabendo a teoria, agora vamos aos exemplos:
true + false // 1
12 / "6" // 2
"number" + 15 + 3 // 'number153'
15 + 3 + "number" // '18number'
[1] > null // true
"foo" + + "bar" // 'fooNaN'
'true' == true // false
false == 'false' // false
null == '' // false
!!"false" == !!"true" // true
['x'] == 'x' // true
[] + null + 1 // 'null1'
[1,2,3] == [1,2,3] // false
{}+[]+{}+[1] // '0[object Object]1'
!+[]+[]+![] // 'truefalse'
new Date(0) - 0 // 0
new Date(0) + 0 // 'Thu Jan 01 1970 02:00:00(EET)0'
Abaixo, você encontrará explicações para cada expressão.
O operador binário +
aciona a conversão numérica gerando o resultado true
ou false
.
true + false
==> 1 + 0
==> 1
O operador aritmético /
aciona a conversão numérico para a string '6'
:
12 / '6'
==> 12 / 6
==>> 2
O operador +
possui uma associação de leitura a partir da esquerda para a direita (left-to-right associativity), portanto a expressão "number" + 15
é executada primeiro. Desde que o primeiro operando é uma string, o operador + aciona a conversão para string do número 15. No segundo passo, a expressão "number15" + 3
é tratada da mesma forma.
“number” + 15 + 3
==> "number15" + 3
==> "number153"
A expressão 15 + 3
é avaliada primeiro. Já que ambos operandos são numéricos, não é preciso fazer a coerção dos tipos. Mas na segunda expressão, quando 18 + 'number'
é avalido, ao verificar que um dos operandos é uma string, ele aciona a conversão para string.
15 + 3 + "number"
==> 18 + "number"
==> "18number"
O operador de comparação >
acionada a conversão numérica para [1]
e null
.
[1] > null
==> '1' > 0
==> 1 > 0
==> true
O operador unário +
tem maior precedência ao operador binário +
. Então a expressão +'bar'
é avaliada primeiro. O operador unário aciona a conversão numérica para a string 'bar'
. Já que a string não apresenta um número válido, o resultado será NaN
. Na segunda etapa, a expressão 'foo' + NaN
será avaliada.
"foo" + + "bar"
==> "foo" + (+"bar")
==> "foo" + NaN
==> "fooNaN"
O operador ==
aciona a conversão numérica, a string true
é convertida para NaN
, o boolean true
é convertido para 1
.
'true' == true
==> NaN == 1
==> false
false == 'false'
==> 0 == NaN
==> false
O operador ==
normalmente aciona a conversão numérica, mas não é o caso quando é colocado null
. null
é igual apenas a null
ou undefined
.
null == ''
==> false
O operador !!
converter ambas strings 'true'
e 'false'
para o boolean true
, já que eles não são strings vazias. Então, ==
apenas verifica a igualdade de dois booleans true
sem qualquer coerção.
!!"false" == !!"true"
==> true == true
==> true
O operador ==
aciona a conversão numérica para um array. O método do array valueOf()
retorna o próprio array, e é ignorado por não ser um primitivo. A função do array toString()
converte ['x']
para a string 'x'
.
['x'] == 'x'
==> 'x' == 'x'
==> true
O operador +
aciona uma conversão numérica para []
. A função do array valueOf()
é ignorado, pois retorna a si mesmo, cujo valor não é primitivo. A função do array toString()
retorna uma string vazia.
Na segunda expressão '' + null + 1
é avaliada.
[] + null + 1
==> '' + null + 1
==> 'null' + 1
==> 'null1'
Os operadores lógicos ||
e &&
fazem coerção para booleano, mas retornando os operandos originais — não valores booleanos. 0
é falso(falsy), enquanto '0'
é verdadeiro(truthy), pois não é uma string vazia. Um objeto vazio {}
também retorna verdadeiro(truthy).
0 || "0" && {}
==> (0 || "0") && {}
==> (false || true) && true // internamente
==> "0" && {}
==> true && true // internamente
==> {}
Não é preciso fazer coerção pois ambos operandos são do mesmo tipo. Desde que ==
verifica a identidade do objeto (object identity), e não sua igualdade (object equality), o resultado será false
, por conta dos 2 arrays serem de instâncias diferentes.
[1,2,3] == [1,2,3]
==> false
Todos os operandos são valores não primitivos, portanto, +
inicia a conversão numérica com o item mais a esquerda. A função valueOf
de ambos objetos e arrays retornarão a si mesmo, e serão ignorados. O método toString()
é usado como fallback. A pegadinha aqui é que {}
não é considerado um objeto literal, mas sim como um bloco de declaração de estado, então é ignorado. A avaliação começará com a próxima expressão +
[]
, que será convertido para uma string vazia através do método toString()
, e então para 0
.
{}+[]+{}+[1]
==> +[]+{}+[1]
==> 0 + {} + [1]
==> 0 + '[object Object]' + [1]
==> '0[object Object]' + [1]
==> '0[object Object]' + '1'
==> '0[object Object]1'
Esse é mais fácil de explicar, pois o passo a passo de sua resolução se dará de acordo com a precedência do operador.
!+[]+[]+![]
==> (!+[]) + [] + (![])
==> !0 + [] + false
==> true + [] + false
==> true + '' + false
==> 'truefalse'
O operador -
acionará a conversão numérica para Date
. A função Date.valueOf()
retornará o número de milissegundos desde a época do Unix.
new Date(0) - 0
==> 0 - 0
==> 0
O operador +
acionará a conversão padrão. Date assumirá uma conversão para string, portanto o método toString()
será utilizado, ao invés do valueOf()
.
new Date(0) + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)' + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)0'
#Rápidas explicações
O que é um operador unário e binário?
- Unário: aquele que interage sobre um elemento. Ex: +, -, ++.
- Binário: aquele que interage sobre dois elementos. Ex: +, -, *, /, &, &&.
Referências
Recomendo o excelente livro “Understanding ES6” escrito por Nicholas C. Zakas. É uma grande fonte para aprender ES6, não é tão avançado, e não fica muito tempo em partes mais profundas.
E aqui um ótimo livro de ES5 — SpeakingJS escrito por Axel Rauschmayer.
(Russian) Современный учебник Javascript — https://learn.javascript.ru/. Especialmente essas duas páginas de coerção.
JavaScript Comparison Table — https://dorey.github.io/JavaScript-Equality-Table/
wtfjs — um pequeno blog sobre aquela linguagem que amamos apesar de nos dar tanto para odiar — https://wtfjs.com/