Conversión de objetos a primitivos en JavaScript
Aprende cómo JavaScript convierte objetos a primitivos: las sugerencias string, number y default, el método Symbol.toPrimitive y la cadena de respaldo toString/valueOf.
Introducción a la conversión de objetos a primitivos
En JavaScript, los objetos son valores de referencia, pero muchas operaciones esperan un primitivo (un string, un número o un boolean). Cuando escribes obj + "", +obj o `${obj}`, el lenguaje debe convertir primero el object en un primitivo antes de ejecutar la operación. Esto se denomina conversión de objeto a primitivo.
Esta guía explica las reglas que sigue JavaScript: las tres sugerencias de conversión ("string", "number", "default"), el método Symbol.toPrimitive que te permite controlar la conversión, y la cadena de respaldo toString()/valueOf() que se usa cuando Symbol.toPrimitive no está presente.
Cómo funciona la conversión de objeto a primitivo
No existe ningún operador que convierta un object a un boolean: los objetos son siempre verdaderos en un contexto boolean. Por tanto, la conversión de objeto a primitivo solo produce un string o un número, y JavaScript decide cuál usar pasándole al object una sugerencia:
- Primero busca un método
[Symbol.toPrimitive](hint). Si está presente, se llama y se usa su valor de retorno (que debe ser un primitivo). - Si
Symbol.toPrimitiveno existe, JavaScript recurre atoString()yvalueOf(), llamándolos en un orden que depende de la sugerencia.
Veremos el respaldo en detalle más adelante. Primero, el enfoque moderno y explícito.
Ejemplo: implementar Symbol.toPrimitive
Explicación: El object user define un único método Symbol.toPrimitive que se ramifica según la sugerencia. Un literal de plantilla solicita la sugerencia "string", la multiplicación solicita "number", y el operador binario + solicita "default". Devolver this.money para el caso predeterminado mantiene la coherencia aritmética con + y *.
Comprender las sugerencias de conversión
Una sugerencia es un string que el motor pasa para indicar al object qué tipo de primitivo prefiere la operación:
"string": se espera que el resultado sea un string —String(obj),`${obj}`,alert(obj), o un object usado como clave de propiedad."number": se espera un resultado numérico —+objunario,obj * 2,obj - 1,obj < other,Number(obj),Math.round(obj)."default": el operador acepta cualquier tipo y no sabe cuál solicitar. Es menos frecuente de lo que se piensa, pero importa: el operador binario+(que puede significar suma y concatenación de strings) usa"default", al igual que los operadores de igualdad débil==/!=al comparar un object con un número o string.
Una sorpresa habitual:
obj + ""no usa la sugerencia"string"— usa"default". Si solo manejas"string"y"number", la rama"default"es la que se ejecuta con+.
Ejemplo: gestionar diferentes sugerencias
Explicación: Aquí el object item gestiona las tres sugerencias. Observa la última línea: como el operador binario + usa la sugerencia "default", item + '' ejecuta la rama "default" — no la rama "string" — produciendo "Item: Chair, Price: 45". Esta es exactamente la sutileza que hace que valga la pena manejar cada sugerencia de forma explícita. Consulta también operadores de comparación y operadores numéricos.
El respaldo toString / valueOf
Si un object no tiene el método Symbol.toPrimitive, JavaScript utiliza el par de métodos más antiguo y elige un orden basado en la sugerencia:
- Para la sugerencia
"string": se intenta primerotoString(), luegovalueOf(). - Para la sugerencia
"number"o"default": se intenta primerovalueOf(), luegotoString().
En cada caso se usa el primer método que devuelve un primitivo; si un método devuelve un object, se omite y se prueba el siguiente. Un object simple hereda Object.prototype.toString (que devuelve "[object Object]") y Object.prototype.valueOf (que devuelve el propio object, por lo que se ignora) — por eso ({}) + "" es "[object Object]".
Explicación: Sin Symbol.toPrimitive, la sugerencia "string" llega a toString() y devuelve "John", mientras que las sugerencias numéricas y predeterminadas llegan a valueOf() y devuelven 1000. Symbol.toPrimitive es preferible en código nuevo porque proporciona un único lugar explícito para gestionar cada sugerencia; toString/valueOf siguen siendo útiles cuando solo te importa una dirección.
Buenas prácticas para usar toPrimitive
Implementar Symbol.toPrimitive de forma eficaz implica combinar claridad, coherencia y pruebas exhaustivas para garantizar que los objetos se comporten de manera predecible al convertirse a primitivos. A continuación se explica cómo aplicar estas buenas prácticas al usar el método Symbol.toPrimitive:
1. Semántica clara
Buena práctica: Define Symbol.toPrimitive de forma clara para que las conversiones del object sean predecibles y comprensibles. Esto implica gestionar explícitamente los diferentes tipos de sugerencias de conversión ("string", "number" y "default") y proporcionar valores de retorno adecuados para cada caso.
Ejemplo:
Explicación: En este ejemplo, el object dateEvent define claramente los comportamientos de conversión para los contextos string y number. Para las conversiones a string devuelve una descripción, y para las conversiones a number devuelve la marca de tiempo del evento. Esta distinción clara ayuda a otros desarrolladores a entender qué esperar al convertir el object en diferentes contextos.
2. Coherencia
Buena práctica: Asegúrate de que las conversiones sean coherentes con los datos del object y su uso previsto, evitando comportamientos confusos o ilógicos.
Explicación: El object product garantiza que la lógica de conversión sea coherente con sus propiedades. Ya sea para convertirlo a string para mostrarlo o a number para cálculos, el resultado sigue siendo intuitivo y útil, respetando el uso previsto de cada propiedad.
3. Pruebas
Buena práctica: Prueba exhaustivamente cómo se comportan tus objetos en distintos escenarios de conversión para evitar errores inesperados en tu aplicación.
Enfoques de prueba de ejemplo:
- Pruebas unitarias: Escribe pruebas unitarias que intenten convertir el object mediante distintas operaciones (como operaciones aritméticas, concatenación de strings o pasar el object a funciones que esperan un tipo primitivo) para garantizar que todos los escenarios devuelven los valores esperados.
// Note: In a browser environment, use console.assert or a test framework like Jest/Mocha.
// Assumes 'product' is defined as in the previous example.
console.assert(String(product) === "Laptop costs $1200", "String conversion failed");
console.assert(+product === 1200, "Number conversion failed");
console.assert(product + '' === "Laptop", "Default conversion failed");Explicación: Mediante pruebas unitarias, puedes verificar que el object product gestiona correctamente todas las formas de conversión según la lógica especificada en Symbol.toPrimitive. Esto ayuda a garantizar la fiabilidad y coherencia en cómo interactúa tu object con distintas partes del motor de JavaScript y tu aplicación.
Errores comunes
- No existe sugerencia boolean. En un contexto boolean (
if (obj),!obj,obj && x) el object es siempre verdadero y nunca se convierte a primitivo. La conversión de objeto a primitivo solo produce strings y números. +usa"default", no"string". Esto confunde a muchos desarrolladores:obj + ""activa la sugerencia predeterminada. Las comparaciones comoobj == 5también usan"default".- Un método debe devolver un primitivo. Si
Symbol.toPrimitive(ovalueOf/toString) devuelve un object en lugar de un primitivo, se produce unTypeError. En el par de respaldo, devolver un object simplemente hace que ese método se omita. - La conversión numérica de un resultado string puede producir
NaN. Si tu rama"number"/"default"devuelve un string no numérico, los contextos que esperan un número obtienenNaN:+{ [Symbol.toPrimitive]: () => "abc" }esNaN.
Conclusión
La conversión de objeto a primitivo es un mecanismo fundamental de JavaScript que permite a los objetos participar en operaciones aritméticas, concatenación de strings y comparaciones. El motor elige una sugerencia ("string", "number" o "default"), intenta primero Symbol.toPrimitive y, en caso contrario, recurre a toString()/valueOf(). Al implementar Symbol.toPrimitive, obtienes un único lugar explícito para controlar cómo se comporta un object personalizado en cada contexto, lo que conduce a un código más predecible y mantenible. Para profundizar, revisa tipos de datos, tipos symbol y métodos de object y this.