<devzera

Aprendendo e compartilhando tecnologia

Explicando a coerção de tipos em Javascript

Saiba como funciona

Coisas estranhas podem acontecer no Javascript

Esse texto é uma tradução livre do artigoJavaScript type coercion explaineddeAlexey Samoshkin.

[Edit 02/05/2018]: Esse post estádisponível em Russo. Palmas aSerj Bulavykpor seus esforços.

Coerção de tipos (type coercion)é o processo de conversão de um valor de um tipo, para outro (como a conversão de uma string para um número, um objeto para um booleano e etc). Qualquer tipo, seja primitivo ou um objeto, é um sujeito válido para coerção de tipo. Para recordar, os primitivos são: number, string, booleano, null, undefined + Symbol (adicionado no ES6).

Como um exemplo de coerção de tipo em prática, veja o linkJavaScript Comparison Table, que mostra como o operador de igualdade==se comporta para diferentes tipos. Essa matriz parece assustadora devido à coerção de tipo implícita que o operador==faz, e dificilmente será possível lembrar de todas essas combinações. E você não precisa fazer isso — apenas aprenda os princípios básicos da coerção de tipos.

Esse artigo vai a fundo de como a coerção de tipos funciona no Javascript, e irá prepará-lo com o conhecimento básico para que você possa sentir-se confiante ao explicar sobre as expressões. Ao final do artigo, mostrarei as respostas e as explicarei.

true + false
12 / "6"
"number" + 15 + 3
15 + 3 + "number"
[1] > null
"foo" + + "bar"
'true' == true
false == 'false'
null == ''
!!"false" == !!"true"
[‘x’] == ‘x’
[] + null + 1
[1,2,3] == [1,2,3]
{}+[]+{}+[1]
!+[]+[]+![]
new Date(0) - 0
new Date(0) + 0

Sim, essa é uma lista boba de coisas que você pode fazer como desenvolvedor. Em 90% dos casos é melhor evitar a coerção de tipos implícita. Considere essa lista como exercícios para aprendizagem para testar seu conhecimento de como a coerção de tipos funciona. Se estiver entendiado, você pode encontrar mais emwtfjs.com.

A propósito, você pode encontrar perguntas disso em entrevistas para vagas de Javascript. Então continue lendo 😄.

Coerção Implícita vs Explícita

Coerção de tipos pode ser explícita ou implícita.

Quando um desenvolvedor deseja converter um tipo escrevendo algo como,Number(valor), isso é chamado decoerção de tipos explícita(explicit type coercionoutype casting.

Já que o Javascript é uma linguagem fracamente tipada (weakly-typed language), valores também podem ser convertidos entre diferentes tipos automaticamente, e isso é chamado decoerção de tipos implícita (implicit type coercion).Isso acontece quando você atribui operados para valores de diferentes tipos, como1 == null,2/’5',null + new Date(), ou isso pode decorrer do contexto, como usarif (value) {…}, ondevalueé forçado a retornar um booleano.

Um operador que não desencadeia a coerção de tipos implítica é===, que é chamado de operador restrito de igualdade (strict equality operator). O operador de igualdade==por outro lado, faz a comparação e ativa a coerção de tipos, se necessário.

Coerção de tipo implícito é uma faca de dois gumes: é uma grande fonte de frustração e defeitos, mas também um mecanismo útil que nos permite escrever menos código sem perder a legibilidade.

Três tipos de conversão

A primeira regra que precisamos saber é que existem apenas 3 tipos de conversão no Javascript:

  • para string;
  • para boolean;
  • para number.

A segunda, é que a lógica para conversão de tipos primitivos e objetos funcionam de forma diferente, mas ambos só podem ser convertido nessas 3 maneiras.

Vamos começar primeiro com os primitivos.

Conversão de String

Para indicar a conversão explícita de valores para string use a funçãoString(). A coerção implícita é ativada pelo operador binário+, quando qualquer operando é uma string:

String(123) // explícito
123 + ''    // implícito

Todos os valores primitivos são convertidos em strings naturalmente, como você poderia esperar:

String(123)                   // '123'
String(-12.3)                 // '-12.3'
String(null)                  // 'null'
String(undefined)             // 'undefined'
String(true)                  // 'true'
String(false)                 // 'false'

A conversão de Symbol é um pouco complicada, porque só pode ser convertida explicitamente, mas não implicitamente. Leia mais nas regras de coerção de tipos doSymbol.

String(Symbol('my symbol'))   // 'Symbol(my symbol)'
'' + Symbol('my symbol')      // TypeError é lançado

Conversão de Boolean

Para indicar a conversão explícita de valores para boolean use a funçãoBoolean(). A conversão implícita ocorre no contexto lógico ou é ativada por operadores lógicos (||&&!) .

Boolean(2)          // explícito
if (2) { ... }      // implícito devido ao contexto lógico
!!2                 // implícito devido ao operador lógico
2 || 'hello'        // implícito devido ao operador lógico

Observação: Operadores lógicos como||e&&fazem conversões booleanas internamente, masna verdade retornam o valor dos operandosoriginais, mesmo que eles não sejam booleanos.

// retorna o número 123 ao invés de true
// 'hello' e 123 são convertidos para boolean internamente para calcular a expressão
let x = 'hello' && 123;   //x === 123 é true

Assim que houver apenas dois resultados possíveis da conversão booleana:trueoufalse, é mais fácil lembrar a lista de valores falsos (falsy values).

Boolean('')           // false
Boolean(0)            // false     
Boolean(-0)           // false
Boolean(NaN)          // false
Boolean(null)         // false
Boolean(undefined)    // false
Boolean(false)        // false

Qualquer valor não inserido nessa lista ao ser convertido serátrue, incluindo objetos, funções,Array,Date, tipos definidos pelo usuário e assim por diante. Symbols são considerados como valores verdadeiros (truthy values). Objetos vazios e arrays também:

Boolean({})             // true
Boolean([])             // true
Boolean(Symbol())       // true
!!Symbol()              // true
Boolean(function() {})  // true

Conversão Numérica

Para uma conversão explícita aplique a funçãoNumber(), assim como feito comBoolean()eString().

A conversão implícita é complicada, pois é acionada em mais casos:

  • operadores de comparação (comparison operators)(>,<,<=,>=)
  • operadores bitwise (|&^~)
  • operadores aritméticos (-+*/%). Saiba que usar+não irá ativar a conversão numérica quando qualquer operando for uma string.
  • operador unário+
  • operador de igualdade==(incl.!=).
    Perceba que==não ativa a conversão numérica quando ambos operandos são strings.
Number('123')   // explícito
+'123'          // implícito
123 != '456'    // implícito
4 > '5'         // implícito
5/null          // implícito
true | 0        // implícito

Abaixo como valores primitivos são convertido para números:

Number(null)                   // 0
Number(undefined)              // NaN
Number(true)                   // 1
Number(false)                  // 0
Number(" 12 ")                 // 12
Number("-12.34")               // -12.34
Number("\n")                   // 0
Number(" 12s ")                // NaN
Number(123)                    // 123

Ao converter uma string em número, a engine primeiro remove os espaços em branco com os caracteres\ne\t, retornandoNaNse a string tratada não representar um número válido. Se a string estiver vazia, retornará0.

nulleundefinedsão tratados de forma diferentes:nullvira 0, enquantoundefinedse tornaNaN.

Symbols não podem ser convertidos em números nem explicitamente nem implicitamente. Além disse,TypeErroré lançado ao invés de silenciosamente converter paraNaN, como acontece paraundefined. Veja mais sobre as regras de conversão de símbolo no MDN.

Number(Symbol('my symbol'))    // TypeError é lançado
+Symbol('123')                 // TypeError é lançado

Existemduas regras especiaispara relembrar:

  1. Quando aplicamos==paranullouundefined, a conversão numérico não ocorre.nullé apenas igual anullouundefined, e não é igual a mais nada.
null == 0               // false, null is not converted to 0
null == null            // true
undefined == undefined  // true
null == undefined       // true

2.NaNnão é igual a nada que não seja ele mesmo:

if (value !== value) { console.log("we're dealing with NaN here") }

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 paratrue, 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 serNumberouString. Esse argumento é opcional.

Ambas conversões, número e string fazem uso de dois métodos do objeto de entrada (input):valueOfetoString. Ambas funções são declaradas noObject.prototypee, portanto, disponível para qualquer tipo derivado, comoDate,Array, e etc.

Em geral, o algoritmo é o seguinte:

  1. Se o input já é do tipo primitivo, retorne-o;
  2. Chame a funçãoinput.toString(), se o resultado for do tipo primitivo, retorne-o;
  3. Chame a funçãoinput.valueOf(), se o resultado for do tipo primitivo, retorne-o;
  4. Se nem a funçãoinput.toString()ouinput.valueOf()retornar um tipo primitivo, lanceTypeError.

Conversões numéricas primeiro chamam a funçãovalueOf(3) com o fallbacktoString(2).

A conversão de string faz exatamente o oposto:toString(2) seguido devalueOf(3).

A maioria dos tipos internos(built-in) não possui a funçãovalueOf, ou possuivalueOfretornando o próprio objeto, então é ignorado por não ser do tipo primitivo. É por isso que a conversão de tiposnumberestringpodem funcionar da mesma forma — ambos acabam chamandotoString().

Operadores diferentes podem acionar a conversão numérica ou de string com a ajuda do parâmetropreferredType. Mas existem duas exceções: o comparador de igualdade abstrato==e a opção binária+acionam modos de conversão padrão (preferredTypenão é especificado, ou igual adefault). Nesse caso, a maior dos tipos internos(built-in) assumirão uma conversão numérica como default, excetoDateque fará uma conversão de string.

Segue abaixo um exemplo de como se comporta uma conversa deDate:

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ãotoString()evalueOf()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 comoobj + ‘’retorna'101'como uma string. O operador+dispara um modo de conversão padrão, e como dito anteriormente,Objectassume a conversão numérico como padrão, usando portanto, o métodovalueOf()ao invés dotoString().

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étodostoStringevalueOf.

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 resultadotrueoufalse.

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ão15 + 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, quando18 + '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]enull.

[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' + NaNserá avaliada.

"foo" + + "bar" 
==> "foo" + (+"bar") 
==> "foo" + NaN 
==> "fooNaN"

O operador==aciona a conversão numérica, a stringtrueé convertida paraNaN, o booleanotrueé 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 é colocadonull.nullé igual apenas anullouundefined.

null == ''
==> false

O operador!!converter ambas strings'true'e'false'para o booleanotrue, já que eles não são strings vazias. Então,==apenas verifica a igualdade de dois booleanostruesem qualquer coerção.

!!"false" == !!"true"  
==> true == true
==> true

O operador==aciona a conversão numérica para um array. O método do arrayvalueOf()retorna o próprio array, e é ignorado por não ser um primitivo. A função do arraytoString()converte['x']para a string'x'.

['x'] == 'x'  
==> 'x' == 'x'
==>  true

O operador+aciona uma conversão numérica para[]. A função do arrayvalueOf()é ignorado, pois retorna a si mesmo, cujo valor não é primitivo. A função do arraytoString()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çãovalueOfde ambos objetos e arrays retornarão a si mesmo, e serão ignorados. O métodotoString()é 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étodotoString(), e então para0.

{}+[]+{}+[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 paraDate. A funçãoDate.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.Dateassumirá uma conversão para string, portanto o métodotoString()será utilizado, ao invés dovalueOf().

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 porNicholas 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 —SpeakingJSwritten porAxel Rauschmayer.

(Russian) Современный учебник Javascript —https://learn.javascript.ru/. Especiallythesetwopages on type coercion.

JavaScript Comparison Table —https://dorey.github.io/JavaScript-Equality-Table/

wtfjs — a little code blog about that language we love despite giving us so much to hate —https://wtfjs.com/

https://codeburst.io/javascript-quickie-what-is-type-coercion-74f19df6d16f

https://medium.com/codezillas/let-me-coerce-you-into-liking-javascripts-dynamic-typing-system-3cd22c19cb64

https://medium.com/intrinsic/javascript-object-type-coercion-b2ec176c02c4

https://hackernoon.com/understanding-js-coercion-ff5684475bfc

Comentários