Explicación de la coerción de tipo JavaScript

Conoce tus motores

[Editar 2/5/2018] : esta publicación ya está disponible en ruso. Aplaude a Serj Bulavyk por sus esfuerzos.

La coerción de tipos es el proceso de convertir un valor de un tipo a otro (como cadena en número, objeto en booleano, etc.). Cualquier tipo, ya sea primitivo o un objeto, es un sujeto válido para la coerción de tipos. Para recordar, las primitivas son: número, cadena, booleano, nulo, indefinido + Símbolo (agregado en ES6).

Como ejemplo de coerción de tipos en la práctica, observe la Tabla de comparación de JavaScript, que muestra cómo se ==comporta el operador de igualdad flexible para diferentes tipos ay b. Esta matriz parece aterradora debido a la coerción de tipo implícita que ==hace el operador, y es casi imposible recordar todas esas combinaciones. Y no tiene que hacer eso, simplemente aprenda los principios de coerción de tipo subyacentes.

Este artículo profundiza en cómo funciona la coerción de tipos en JavaScript y le proporcionará los conocimientos esenciales para que pueda sentirse seguro al explicar qué se calculan las siguientes expresiones. Al final del artículo, mostraré las respuestas y las explicaré.

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

Sí, esta lista está llena de cosas bastante tontas que puedes hacer como desarrollador. En el 90% de los casos de uso, es mejor evitar la coerción de tipo implícita. Considere esta lista como un ejercicio de aprendizaje para poner a prueba sus conocimientos sobre cómo funciona la coerción de tipos. Si está aburrido, puede encontrar más ejemplos en wtfjs.com.

Por cierto, a veces puede enfrentarse a este tipo de preguntas en la entrevista para un puesto de desarrollador de JavaScript. Entonces, ¿sigues leyendo?

Coerción implícita versus explícita

La coerción de tipo puede ser explícita e implícita.

Cuando un desarrollador expresa la intención de convertir entre tipos escribiendo el código apropiado Number(value), se llama coerción explícita de tipos (o conversión de tipos).

Dado que JavaScript es un lenguaje de tipo débil, los valores también se pueden convertir entre diferentes tipos de forma automática, y se denomina coerción de tipo implícita . Suele ocurrir cuando aplica operadores a valores de diferentes tipos, como

1 == null, 2/’5', null + new Date(), O puede ser desencadenada por el contexto que rodea, como con if (value) {…}, donde valuese fuerza a booleano.

Un operador que no desencadena la coerción de tipo implícita es ===, que se denomina operador de igualdad estricta. El operador de igualdad flexible, ==por otro lado, hace tanto la comparación como la coerción de tipos si es necesario.

La coerción de tipo implícita es una espada de doble filo: es una gran fuente de frustración y defectos, pero también un mecanismo útil que nos permite escribir menos código sin perder la legibilidad.

Tres tipos de conversión

La primera regla que debe saber es que solo hay tres tipos de conversión en JavaScript:

  • Encadenar
  • a booleano
  • Al numero

En segundo lugar, la lógica de conversión para primitivas y objetos funciona de manera diferente, pero tanto las primitivas como los objetos solo se pueden convertir de esas tres formas.

Comencemos primero con las primitivas.

Conversión de cadenas

Para convertir explícitamente valores en una cadena, aplique la String()función. La coerción implícita es activada por el +operador binario , cuando cualquier operando es una cadena:

String(123) // explicit 123 + '' // implicit

Todos los valores primitivos se convierten en cadenas de forma natural, como es de esperar:

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

La conversión de símbolos es un poco complicada, porque solo se puede convertir explícitamente, pero no implícitamente. Lea más sobre las Symbolreglas de coerción.

String(Symbol('my symbol')) // 'Symbol(my symbol)' '' + Symbol('my symbol') // TypeError is thrown

Conversión booleana

Para convertir explícitamente un valor en un valor booleano, aplique la Boolean()función.

La conversión implícita ocurre en un contexto lógico o se activa mediante operadores lógicos ( ||&&!).

Boolean(2) // explicit if (2) { ... } // implicit due to logical context !!2 // implicit due to logical operator 2 || 'hello' // implicit due to logical operator

Nota : Los operadores lógicos como ||y &&realizan conversiones booleanas internamente, pero en realidad devuelven el valor de los operandos originales, incluso si no son booleanos.

// returns number 123, instead of returning true // 'hello' and 123 are still coerced to boolean internally to calculate the expression let x = 'hello' && 123; // x === 123

Tan pronto como solo haya 2 resultados posibles de conversión booleana: trueo false, es más fácil recordar la lista de valores falsos.

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

Cualquier valor que no está en la lista se convierte en true, incluyendo objeto, función Array, Date, tipo definido por el usuario, y así sucesivamente. Los símbolos son valores veraces. El objeto vacío y las matrices también son valores veraces:

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

Conversión numérica

Para una conversión explícita, simplemente aplique la Number()función, igual que hizo con Boolean()y String().

La conversión implícita es complicada porque se activa en más casos:

  • operadores de comparación ( >, <, <=, >=)
  • operadores bit a bit ( |&^~)
  • operadores aritméticos ( -+*/%). Tenga en cuenta que el binario +no activa la conversión numérica cuando cualquier operando es una cadena.
  • +operador unario
  • operador de igualdad suelto ==(incl. !=).

    Tenga en cuenta que ==no activa la conversión numérica cuando ambos operandos son cadenas.

Number('123') // explicit +'123' // implicit 123 != '456' // implicit 4 > '5' // implicit 5/null // implicit true | 0 // implicit

Así es como los valores primitivos se convierten en 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

Al convertir una cadena en un número, el motor primero recorta espacio inicial y final, \n, \tcaracteres, volviendo NaNsi la cadena recortada no representa un número válido. Si la cadena está vacía, regresa 0.

nully undefinedse manejan de manera diferente: se nullconvierte 0, mientras que se undefinedconvierte NaN.

Los símbolos no se pueden convertir a un número ni explícita ni implícitamente. Además, TypeErrorse lanza, en lugar de convertirse silenciosamente a NaN, como sucede undefined. Obtenga más información sobre las reglas de conversión de símbolos en MDN.

Number(Symbol('my symbol')) // TypeError is thrown +Symbol('123') // TypeError is thrown

Hay dos reglas especiales para recordar:

  1. Al aplicar ==a nullo undefined, la conversión numérica no ocurre. nulles igual a nullo undefined, y no es igual a ninguna otra cosa.
null == 0 // false, null is not converted to 0 null == null // true undefined == undefined // true null == undefined // true

2. NaN no es igual a nada ni siquiera a sí mismo:

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

Tipo de coerción para objetos

Hasta ahora, hemos analizado la coerción de tipos para valores primitivos. Eso no es muy emocionante.

When it comes to objects and engine encounters expression like [1] + [2,3], first it needs to convert an object to a primitive value, which is then converted to the final type. And still there are only three types of conversion: numeric, string and boolean.

The simplest case is boolean conversion: any non-primitive value is always

coerced to true, no matter if an object or an array is empty or not.

Objects are converted to primitives via the internal [[ToPrimitive]] method, which is responsible for both numeric and string conversion.

Here is a pseudo implementation of [[ToPrimitive]] method:

[[ToPrimitive]] is passed with an input value and preferred type of conversion: Number or String. preferredType is optional.

Both numeric and string conversion make use of two methods of the input object: valueOf and toString . Both methods are declared on Object.prototype and thus available for any derived types, such as Date, Array, etc.

In general the algorithm is as follows:

  1. If input is already a primitive, do nothing and return it.

2. Call input.toString(), if the result is primitive, return it.

3. Call input.valueOf(), if the result is primitive, return it.

4. If neither input.toString() nor input.valueOf() yields primitive, throw TypeError.

Numeric conversion first calls valueOf (3) with a fallback to toString (2). String conversion does the opposite: toString (2) followed by valueOf (3).

Most built-in types do not have valueOf, or have valueOf returning this object itself, so it’s ignored because it’s not a primitive. That’s why numeric and string conversion might work the same — both end up calling toString().

Different operators can trigger either numeric or string conversion with a help of preferredType parameter. But there are two exceptions: loose equality == and binary + operators trigger default conversion modes (preferredType is not specified, or equals to default). In this case, most built-in types assume numeric conversion as a default, except Date that does string conversion.

Here is an example of Date conversion behavior:

You can override the default toString() and valueOf() methods to hook into object-to-primitive conversion logic.

Notice how obj + ‘’ returns ‘101’ as a string. + operator triggers a default conversion mode, and as said before Object assumes numeric conversion as a default, thus using the valueOf() method first instead of toString().

ES6 Symbol.toPrimitive method

In ES5 you can hook into object-to-primitive conversion logic by overriding toString and valueOf methods.

In ES6 you can go farther and completely replace internal[[ToPrimitive]] routine by implementing the[Symbol.toPrimtive] method on an object.

Examples

Armed with the theory, now let’s get back to our examples:

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'

Below you can find explanation for each the expression.

Binary + operator triggers numeric conversion for true and false

true + false ==> 1 + 0 ==> 1

Arithmetic division operator / triggers numeric conversion for string '6' :

12 / '6' ==> 12 / 6 ==>> 2

Operator + has left-to-right associativity, so expression "number" + 15 runs first. Since one operand is a string, + operator triggers string conversion for the number 15. On the second step expression "number15" + 3 is evaluated similarly.

“number” + 15 + 3 ==> "number15" + 3 ==> "number153"

Expression 15 + 3 is evaluated first. No need for coercion at all, since both operands are numbers. On the second step, expression 18 + 'number' is evaluated, and since one operand is a string, it triggers a string conversion.

15 + 3 + "number" ==> 18 + "number" ==> "18number"

Comparison operator &gt; triggers numeric conversion for [1] and null .

[1] > null ==> '1' > 0 ==> 1 > 0 ==> true

Unary + operator has higher precedence over binary + operator. So +'bar' expression evaluates first. Unary plus triggers numeric conversion for string 'bar'. Since the string does not represent a valid number, the result is NaN. On the second step, expression 'foo' + NaN is evaluated.

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

== operator triggers numeric conversion, string 'true' is converted to NaN, boolean true is converted to 1.

'true' == true ==> NaN == 1 ==> false false == 'false' ==> 0 == NaN ==> false

== usually triggers numeric conversion, but it’s not the case with null . null equals to null or undefined only, and does not equal to anything else.

null == '' ==> false

!! operator converts both 'true' and 'false' strings to boolean true, since they are non-empty strings. Then, == just checks equality of two boolean true's without any coercion.

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

== operator triggers a numeric conversion for an array. Array’s valueOf() method returns the array itself, and is ignored because it’s not a primitive. Array’s toString() converts ['x'] to just 'x' string.

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

+ operator triggers numeric conversion for []. Array’s valueOf() method is ignored, because it returns array itself, which is non-primitive. Array’s toString returns an empty string.

On the the second step expression '' + null + 1 is evaluated.

[] + null + 1 ==> '' + null + 1 ==> 'null' + 1 ==> 'null1'

Logical || and && operators coerce operands to boolean, but return original operands (not booleans). 0 is falsy, whereas '0' is truthy, because it’s a non-empty string. {} empty object is truthy as well.

0 || "0" && {} ==> (0 || "0") && {} ==> (false || true) && true // internally ==> "0" && {} ==> true && true // internally ==> {}

No coercion is needed because both operands have same type. Since == checks for object identity (and not for object equality) and the two arrays are two different instances, the result is false.

[1,2,3] == [1,2,3] ==> false

All operands are non-primitive values, so + starts with the leftmost triggering numeric conversion. Both Object’s and Array’svalueOf method returns the object itself, so it’s ignored. toString() is used as a fallback. The trick here is that first {} is not considered as an object literal, but rather as a block declaration statement, so it’s ignored. Evaluation starts with next +[] expression, which is converted to an empty string via toString() method and then to 0 .

{}+[]+{}+[1] ==> +[]+{}+[1] ==> 0 + {} + [1] ==> 0 + '[object Object]' + [1] ==> '0[object Object]' + [1] ==> '0[object Object]' + '1' ==> '0[object Object]1'

This one is better explained step by step according to operator precedence.

!+[]+[]+![] ==> (!+[]) + [] + (![]) ==> !0 + [] + false ==> true + [] + false ==> true + '' + false ==> 'truefalse'

- operator triggers numeric conversion for Date. Date.valueOf() returns number of milliseconds since Unix epoch.

new Date(0) - 0 ==> 0 - 0 ==> 0

+ operator triggers default conversion. Date assumes string conversion as a default one, so toString() method is used, rather than 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'

Resources

I really want to recommend the excellent book “Understanding ES6” written by Nicholas C. Zakas. It’s a great ES6 learning resource, not too high-level, and does not dig into internals too much.

And here is a good book on ES5 only - SpeakingJS written by Axel Rauschmayer.

(Russian) Современный учебник Javascript — //learn.javascript.ru/. Especially these two pages on type coercion.

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

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