¿Qué sucede cuando se agregan objetos obj1 + obj2
, se restan obj1 - obj2
o se imprimen usando alert(obj)
?
JavaScript no le permite personalizar cómo trabajan los operadores en los objetos. A diferencia de otros lenguajes de programación, como Ruby o C++, no podemos implementar un método de objeto especial para manejar la suma (u otros operadores).
En el caso de tales operaciones, los objetos se convierten automáticamente en primitivos y luego la operación se lleva a cabo sobre estas primitivas y da como resultado un valor primitivo.
Esa es una limitación importante: ¡el resultado de obj1 + obj2
(u otra operación matemática) no puede ser otro objeto!
Por ejemplo, no podemos crear objetos que representen vectores o matrices (o logros o lo que sea), agregarlos y esperar un objeto "sumado" como resultado. Estas hazañas arquitectónicas quedan automáticamente “fuera de lugar”.
Entonces, debido a que técnicamente no podemos hacer mucho aquí, no hay matemáticas con objetos en proyectos reales. Cuando esto sucede, salvo raras excepciones, se debe a un error de codificación.
En este capítulo cubriremos cómo un objeto se convierte en primitivo y cómo personalizarlo.
Tenemos dos propósitos:
Date
). Los encontraremos más tarde.En el capítulo Conversiones de tipos hemos visto las reglas para conversiones numéricas, de cadenas y booleanas de primitivas. Pero dejamos un hueco para los objetos. Ahora que sabemos acerca de los métodos y símbolos, es posible completarlo.
true
en un contexto booleano, así de simple. Sólo existen conversiones numéricas y de cadenas.Date
(que se tratarán en el capítulo Fecha y hora) se pueden restar y el resultado de date1 - date2
es la diferencia horaria entre dos fechas.alert(obj)
y en contextos similares.Podemos implementar la conversión numérica y de cadenas nosotros mismos, utilizando métodos de objetos especiales.
Ahora entremos en detalles técnicos, porque es la única manera de cubrir el tema en profundidad.
¿Cómo decide JavaScript qué conversión aplicar?
Hay tres variantes de conversión de tipos, que ocurren en diversas situaciones. Se llaman "pistas", como se describe en la especificación:
"string"
Para una conversión de objeto a cadena, cuando realizamos una operación en un objeto que espera una cadena, como alert
:
// output alert(obj); // using object as a property key anotherObj[obj] = 123;
"number"
Para una conversión de objeto a número, como cuando hacemos matemáticas:
// explicit conversion let num = Number(obj); // maths (except binary plus) let n = +obj; // unary plus let delta = date1 - date2; // less/greater comparison let greater = user1 > user2;
La mayoría de las funciones matemáticas integradas también incluyen dicha conversión.
"default"
Ocurre en casos raros cuando el operador “no está seguro” de qué tipo esperar.
Por ejemplo, binario plus +
puede funcionar tanto con cadenas (las concatena) como con números (las suma). Entonces, si un binario plus obtiene un objeto como argumento, utiliza la sugerencia "default"
para convertirlo.
Además, si se compara un objeto usando ==
con una cadena, número o símbolo, tampoco está claro qué conversión se debe realizar, por lo que se usa la sugerencia "default"
.
// binary plus uses the "default" hint let total = obj1 + obj2; // obj == number uses the "default" hint if (user == 1) { ... };
Los operadores de comparación mayor y menor, como <
>
, también pueden funcionar tanto con cadenas como con números. Aun así, utilizan la sugerencia "number"
, no "default"
. Eso es por razones históricas.
Sin embargo, en la práctica las cosas son un poco más sencillas.
Todos los objetos integrados excepto un caso (objeto Date
, lo aprenderemos más adelante) implementan la conversión "default"
de la misma manera que "number"
. Y probablemente deberíamos hacer lo mismo.
Aún así, es importante conocer las 3 pistas, pronto veremos por qué.
Para realizar la conversión, JavaScript intenta encontrar y llamar a tres métodos de objeto:
obj[Symbol.toPrimitive](hint)
: el método con la clave simbólica Symbol.toPrimitive
(símbolo del sistema), si dicho método existe,"string"
obj.toString()
o obj.valueOf()
, lo que exista."number"
o "default"
obj.valueOf()
o obj.toString()
, lo que exista. Empecemos por el primer método. Hay un símbolo incorporado llamado Symbol.toPrimitive
que debe usarse para nombrar el método de conversión, como este:
obj[Symbol.toPrimitive] = function(hint) { // here goes the code to convert this object to a primitive // it must return a primitive value // hint = one of "string", "number", "default" };
Si el método Symbol.toPrimitive
existe, se usa para todas las sugerencias y no se necesitan más métodos.
Por ejemplo, aquí el objeto user
lo implementa:
let user = { name: "John", money: 1000, [Symbol.toPrimitive](hint) { alert(`hint: ${hint}`); return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; // conversions demo: alert(user); // hint: string -> {name: "John"} alert(+user); // hint: number -> 1000 alert(user + 500); // hint: default -> 1500
Como podemos ver en el código, user
se convierte en una cadena autodescriptiva o una cantidad de dinero, según la conversión. El método único user[Symbol.toPrimitive]
maneja todos los casos de conversión.
Si no hay ningún Symbol.toPrimitive
, JavaScript intenta encontrar métodos toString
y valueOf
:
"string"
: llame al método toString
, y si no existe o si devuelve un objeto en lugar de un valor primitivo, llame valueOf
(por lo que toString
tiene prioridad para las conversiones de cadenas).valueOf
, y si no existe o si devuelve un objeto en lugar de un valor primitivo, llame toString
(por lo que valueOf
tiene prioridad para las matemáticas). Los métodos toString
y valueOf
provienen de la antigüedad. No son símbolos (los símbolos no existían hace tanto tiempo), sino métodos "normales" con nombres de cadenas. Proporcionan una forma alternativa "a la antigua" de implementar la conversión.
Estos métodos deben devolver un valor primitivo. Si toString
o valueOf
devuelve un objeto, entonces se ignora (igual que si no hubiera ningún método).
De forma predeterminada, un objeto simple tiene los siguientes métodos toString
y valueOf
:
toString
devuelve una cadena "[object Object]"
.valueOf
devuelve el objeto en sí.Aquí está la demostración:
let user = {name: "John"}; alert(user); // [object Object] alert(user.valueOf() === user); // true
Entonces, si intentamos usar un objeto como una cadena, como en una alert
, entonces, de forma predeterminada, vemos [object Object]
.
El valueOf
predeterminado de se menciona aquí sólo para que esté completo y evitar cualquier confusión. Como puede ver, devuelve el objeto en sí y, por lo tanto, se ignora. No me pregunten por qué, es por razones históricas. Entonces podemos asumir que no existe.
Implementemos estos métodos para personalizar la conversión.
Por ejemplo, aquí user
hace lo mismo que arriba usando una combinación de toString
y valueOf
en lugar de Symbol.toPrimitive
:
let user = { name: "John", money: 1000, // for hint="string" toString() { return `{name: "${this.name}"}`; }, // for hint="number" or "default" valueOf() { return this.money; } }; alert(user); // toString -> {name: "John"} alert(+user); // valueOf -> 1000 alert(user + 500); // valueOf -> 1500
Como podemos ver, el comportamiento es el mismo que en el ejemplo anterior con Symbol.toPrimitive
.
A menudo queremos un único lugar que pueda manejar todas las conversiones primitivas. En este caso, podemos implementar toString
únicamente, así:
let user = { name: "John", toString() { return this.name; } }; alert(user); // toString -> John alert(user + 500); // toString -> John500
En ausencia de Symbol.toPrimitive
y valueOf
, toString
manejará todas las conversiones primitivas.
Lo importante que hay que saber acerca de todos los métodos de conversión de primitivas es que no necesariamente devuelven la primitiva "insinuada".
No hay control sobre si toString
devuelve exactamente una cadena o si el método Symbol.toPrimitive
devuelve un número para la sugerencia "number"
.
Lo único obligatorio: estos métodos deben devolver una primitiva, no un objeto.
Por razones históricas, si toString
o valueOf
devuelve un objeto, no hay error, pero dicho valor se ignora (como si el método no existiera). Esto se debe a que en la antigüedad no existía un buen concepto de "error" en JavaScript.
Por el contrario, Symbol.toPrimitive
es más estricto y debe devolver una primitiva; de lo contrario, se producirá un error.
Como ya sabemos, muchos operadores y funciones realizan conversiones de tipos, por ejemplo, multiplicación *
convierte operandos en números.
Si pasamos un objeto como argumento, hay dos etapas de cálculo:
Por ejemplo:
let obj = { // toString handles all conversions in the absence of other methods toString() { return "2"; } }; alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number
obj * 2
primero convierte el objeto en primitivo (es decir, una cadena "2"
)."2" * 2
se convierte en 2 * 2
(la cadena se convierte en número).Binary plus concatenará cadenas en la misma situación, ya que acepta con gusto una cadena:
let obj = { toString() { return "2"; } }; alert(obj + 2); // "22" ("2" + 2), conversion to primitive returned a string => concatenation
La conversión de objeto a primitiva es llamada automáticamente por muchas funciones y operadores integrados que esperan una primitiva como valor.
Hay 3 tipos (sugerencias):
"string"
(para alert
y otras operaciones que necesitan una cadena)"number"
(para matemáticas)"default"
(pocos operadores, generalmente los objetos lo implementan de la misma manera que "number"
)La especificación describe explícitamente qué operador utiliza qué sugerencia.
El algoritmo de conversión es:
obj[Symbol.toPrimitive](hint)
si el método existe,"string"
obj.toString()
o obj.valueOf()
, lo que exista."number"
o "default"
obj.valueOf()
o obj.toString()
, lo que exista.Todos estos métodos deben devolver una primitiva para que funcione (si está definida).
En la práctica, a menudo es suficiente implementar solo obj.toString()
como un método "general" para conversiones de cadenas que debería devolver una representación "legible por humanos" de un objeto, para fines de registro o depuración.