Explicando a coerção de tipos em Javascript
Saiba como funciona
Esse texto é uma tradução livre do artigo JavaScript type coercion explained de Alexey Samoshkin.
[Edit 02/05/2018]: Esse post está disponível em Russo. Palmas a Serj Bulavyk por 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 link JavaScript 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 em wtfjs.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 de coerção de tipos explícita (explicit type coercion ou type 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 de coerção de tipos implícita (implicit type coercion). Isso acontece quando você atribui operados para valores de diferentes tipos, como 1 == null
, 2/’5'
, null + new Date()
, ou isso pode decorrer do contexto, como usar if (value) {…}
, onde value
é 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ção String()
. 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 do Symbol.
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ção Boolean()
. 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, mas na verdade retornam o valor dos operandos originais, 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: true
ou false
, é 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ção Number()
, assim como feito com Boolean()
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 \n
e \t
, retornando NaN
se a string tratada não representar um número válido. Se a string estiver vazia, retornará 0
.
null
e undefined
são tratados de forma diferentes: null
vira 0, enquanto undefined
se torna NaN
.
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 para NaN
, como acontece para undefined
. 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
Existem duas regras especiais para relembrar:
- Quando aplicamos
==
paranull
ouundefined
, a conversão numérico não ocorre.null
é apenas igual anull
ouundefined
, 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. NaN
nã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 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]]
:
[[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
e, 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;
2. Chame a funçãoinput.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çãoinput.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
:
Você pode sobrescrever os métodos padrãotoString()
e valueOf()
para conectar-se à lógica de conversão objeto para primitivo(object-to-primitive).
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.
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 booleano true
é convertido para 1.
'true' == true
==> NaN == 1
==> falsefalse == '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 booleano true
, já que eles não são strings vazias. Então, ==
apenas verifica a igualdade de dois booleanos 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 written por Axel Rauschmayer.
(Russian) Современный учебник Javascript — https://learn.javascript.ru/. Especially these two pages 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/