BLOG

Featured image of post Entiende la coerción (y los memes) de JavaScript

Entiende la coerción (y los memes) de JavaScript

La coerción en JS la usamos queramos o no, y si ya lo hacemos, por qué no entender cómo funciona?

Abstract Operations

Abstract Operations es básicamente lo que se encarga de hacer la conversión de tipos.

Estas operaciones abstractas, funcionan dentro del engine de JS y no pueden ser llamadas por el usuario.

Type Conversion (a.k.a coercion)

El lenguaje ECMAScript realiza implícitamente la conversión automática de tipos según sea necesario. Para aclarar la semántica de ciertas construcciones, es útil definir un conjunto de operaciones abstractas de conversión. Las operaciones abstractas de conversión son polimórficas; pueden aceptar un valor de cualquier tipo de lenguaje ECMAScript. Pero no se utilizan otros tipos de especificación con estas operaciones.

El tipo BigInt no tiene conversiones implícitas en el lenguaje ECMAScript; los programadores deben llamar a BigInt explícitamente para convertir valores de otros tipos.


ToPrimitive(hint)

Primero, necesitamos llamar a ToPrimitive. Obviamente, si no tenemos un valor primitivo necesitamos convertirlo en uno.

Entonces, si tenemos un valor no primitivo, como un objeto, arrays, funciones etc.. necesitamos hacerlo primitivo

¿Como funciona?

En el, existen 2 métodos que están disponibles para cualquier tipo no primitivo (cualquier función, objeto, array, etc):

Primero invoca valueOf() y si regresa un primitivo, terminamos, si no me da un primitivo o no existe, entonces probamos toString(), donde obtenemos un primitivo o no. Si intentamos ambos, y no obtenemos un primitivo, obtendremos un error.

Igual que en ejemplo anterior, solamente que funciona al revés, primero toString() y después valueOf()

Si vas a utilizar un valor NO primitivo en algún lugar donde se requieren valores primitivos (como concatenación o matemáticas), tus valores van a pasar por el algoritmo ToPrimitive(hint)


ToString(argument)

La operación abstracta ToString, toma cualquier valor y devuelve la representación del valor en forma de string

Los resultados mostrados se pueden testear de la siguiente forma en la consola: String(argument)

Argument Type Result
Undefined Return “undefined”.
Null Return “null”.
Boolean If argument is true, return “true”. If argument is false, return “false”.
Number Return ! Number::toString(argument).
String Return argument.
Symbol Throw a TypeError exception.
BigInt Return ! BigInt::toString(argument).
Object Apply the following steps:
1. Let primValue be ? ToPrimitive(argument, string).
2. Return ? ToString(primValue).
1
2
3
4
5
6
7
8
9
| Argument  | Result      |
| --------- | ----------- |
| undefined | "undefined" |
| null      | "null"      |
| true      | "true"      |
| false     | "false"     |
| 10.3213   | "10.3213"   |
| 0         | "0"         |
| -0        | "0"         |

Pero ¿y si le pasamos un objeto?

Primero va a invocar a ToPrimitive(string) con string hint.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
| Argument                   | Result            |
| -------------------------- | ----------------- |
| []                         | ""                |
| [null, undefined]          | ","               |
| [1,2,3]                    | "1,2,3"           |
| [[],[],[]],[]]             | ",,,"             |
| [,,,,]                     | ",,,"             |
| -----------------          | -------           |
| {}                         | "[object Object]" |
| {"yep": false}             | "[object Object]" |
| {toString(){return "lol"}} | "lol"             |

ToNumber(argument)

Los resultados mostrados se pueden testear de la siguiente forma en la consola: Number(argument)

Argument Type Result
Undefined Return NaN.
Null Return +0𝔽.
Boolean If argument is true, return 1𝔽. If argument is false, return +0𝔽.
Number Return argument (no conversion).
String Return ! StringToNumber(argument).
Symbol Throw a TypeError exception.
BigInt Throw a TypeError exception.
Object Apply the following steps:
1. Let primValue be ? ToPrimitive(argument, number).
2. Return ? ToNumber(primValue).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
| Argument  | Result  |
| --------- | ------- |
| ""        | 0       |
| "0"       | 0       |
| "-0"      | -0      |
| " 001 "   | 1       |
| "10.3213" | 10.3213 |
| "0."      | 0       |
| ".0"      | 0       |
| "."       | NaN     |
| "0xaf"    | 175     |
| ------    | ---     |
| false     | 0       |
| true      | 1       |
| null      | 0       |
| undefined | NaN     |

Pero ¿y si le pasamos un objeto?

Primero va a invocar a ToPrimitive(number) con number hint.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
| Argument              | Result  |
| --------------------- | ------- |
| [""]                  | 0       |
| ["0"]                 | 0       |
| ["-0"]                | -0      |
| [null]                | 0       |
| [undefined]           | 0       |
| [1,2,3]               | NaN     |
| [[[[]]]]              | 0       |
| -----------------     | ------- |
| {..}                  | NaN     |
| {valueOf(){return 1}} | 1       |

Ok, ¿qué paso aquí? Veamos el primer ejemplo:

Recordemos que primero se va a invocar ToPrimitive(number), donde obtendremos "", y al pasarlo a ToNumber, obtenemos 0.

[""] => "" => 0

Para testear más valores, simplemente hay que usar esta pequeña función:

1
2
3
4
5
function testToNumber(argument) {
  return String(Number(argument));
}

testToNumber([""]); //0

ToBoolean(argument)

Siempre que tengas cualquier valor que no sea Boolean y lo estes usando en donde se requiere un Boolean, este algoritmo se va a invocar.

El algoritmo solamente mira una tabla:

Argument Type Result
Undefined Return false.
Null Return false.
Boolean returnargument.
Number If argument is +0𝔽, -0𝔽, or NaN, return false; otherwise return true.
String If argument is the empty String (its length is 0), return false; otherwise return true.
Symbol Return true
BigInt If argument is 0ℤ, return false; otherwise return true. ` exception.
Object Return true
1
2
3
4
5
6
7
8
9
| Falsy     | Truthy                                 |
| --------- | -------------------------------------- |
| ""        | "0"                                    |
| 0, -0     | 23                                     |
| NaN       | {1:2}                                  |
| null      | [1,2],[]                               |
| undefined | function(){...}                        |
| false     | true                                   |
|           | Cualquier valor que no esté en `Falsy` |

Cuando se trabaja con Booleans, no ocurre otra coerción, nunca invocamos ToPrimitive, por eso, [] es true


Los signos

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Number(true); //1
Number(false); //0

1 < 2; //true
2 < 3; //true
1 < 2 < 3; //true

1 < 2 < 3;
true < 3; //true

////////////////////

3 > 2; //true
3 > 1; //true
3 > 2 > 1; //false

3 > 2 > 1;
true > 1;
1 > 1; //false

Es importante saber que cuando trabajamos con +, se espera que se sumen numbers o strings, por eso, al momento de pasar valores no primitivos, como lo es []+[], obtenemos "" pues se hace la coerción a string. Lo mismo sucede con 9+"1",

Al trabajar con -, nuestros valores hacen coerción a number (y no a string o number, como con +), es por esto por lo que 91-"1" es 90 y "ex"-1 es NaN


NaN

NaN es Not A Number, pero es mejor pensar que es Invalid Number, y por supuesto, NaN es de tipo “number”.

NaN es el único valor en todo el engine de JS que no es igual a sí mismo.

1
2
console.log(NaN === NaN); //false
console.log(NaN == NaN); //false

Para comparar NaN (y otros valores), tenemos que usar Object.is

1
console.log(Object.is(NaN, NaN)); //true

Aparte de estos 4, existen más ToSomething, pero estos son los pricipales.


Ejemplos obtenidos de Kyle Simpson