Executando verificação de segurança...
1

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 ou String. 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:

  1. Se o input já é do tipo primitivo, retorne-o;

  2. Chame a função input.toString(), se o resultado for do tipo primitivo, retorne-o;

  3. Chame a função input.valueOf(), se o resultado for do tipo primitivo, retorne-o;

  4. Se nem a função input.toString() ou input.valueOf() retornar um tipo primitivo, lance TypeError.

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/

Carregando publicação patrocinada...