valueOf () frente a toString () en Javascript

115

En Javascript, cada objeto tiene un método valueOf () y toString (). Pensé que el método toString () se invocaba cada vez que se solicitaba una conversión de cadena, pero aparentemente valueOf () lo supera.

Por ejemplo, el código

var x = {toString: function() {return "foo"; },
         valueOf: function() {return 42; }};
window.console.log ("x="+x);
window.console.log ("x="+x.toString());

imprimirá

x=42
x=foo

Esto me parece al revés ... si x fuera un número complejo, por ejemplo, me gustaría que valueOf () me diera su magnitud, pero siempre que quisiera convertir a una cadena, querría algo como "a + bi". Y no me gustaría tener que llamar a toString () explícitamente en contextos que implican una cadena.

¿Es así como es?

cerebro atascado
fuente
6
¿Has probado window.console.log (x);o alert (x);?
Li0liQ
5
Dan "Object" y "foo" respectivamente. Cosas divertidas.
brainjam
En realidad, alerta (x); da "foo" y window.console.log (x); da "foo {}" en Firebug y el objeto completo en la consola de Chrome.
Brainjam
En Firefox 33.0.2 se alert(x)muestra fooy window.console.log(x)muestra Object { toString: x.toString(), valueOf: x.valueOf() }.
John Sonderson

Respuestas:

107

La razón por la que ("x =" + x) da "x = valor" y no "x = una cadena" es la siguiente. Al evaluar "+", JavaScript primero recopila los valores primitivos de los operandos y luego decide si se debe aplicar la suma o la concatenación, en función del tipo de cada primitiva.

Entonces, así es como crees que funciona

a + b:
    pa = ToPrimitive(a)
    if(pa is string)
       return concat(pa, ToString(b))
    else
       return add(pa, ToNumber(b))

y esto es lo que realmente pasa

a + b:
    pa = ToPrimitive(a)
    pb = ToPrimitive(b)*
    if(pa is string || pb is string)
       return concat(ToString(pa), ToString(pb))
    else
       return add(ToNumber(pa), ToNumber(pb))

Es decir, toString se aplica al resultado de valueOf, no a su objeto original.

Para obtener más información, consulte la sección 11.6.1 El operador de suma (+) en la Especificación del lenguaje ECMAScript.


* Cuando se llama en un contexto de cadenas, ToPrimitive hace invocar toString, pero este no es el caso aquí, porque '+' no cumplir cualquier tipo de contexto.

usuario187291
fuente
3
¿No debería el condicional en el bloque "realmente" leer "si (pa es cadena && pb es cadena)"? Es decir, "&&" en lugar de "||" ?
Brainjam
3
El estándar definitivamente dice "o" (ver el enlace).
user187291
2
Sí, eso es exactamente correcto: se da prioridad a las cadenas sobre otros tipos en la concatenación. Si alguno de los operandos es una cadena, todo se concatenará como una cadena. Buena respuesta.
devios1
76

Aquí hay un poco más de detalle, antes de llegar a la respuesta:

var x = {
    toString: function () { return "foo"; },
    valueOf: function () { return 42; }
};

alert(x); // foo
"x=" + x; // "x=42"
x + "=x"; // "42=x"
x + "1"; // 421
x + 1; // 43
["x=", x].join(""); // "x=foo"

La toStringfunción no es "superada" valueOfen general. El estándar ECMAScript realmente responde bastante bien a esta pregunta. Cada objeto tiene una [[DefaultValue]]propiedad, que se calcula bajo demanda. Al preguntar por esta propiedad, el intérprete también proporciona una "pista" sobre el tipo de valor que espera. Si la pista es String, entonces toStringse usa antes valueOf. Pero, si la sugerencia es Number, entonces valueOfse usará primero. Tenga en cuenta que si solo está presente uno, o devuelve un no primitivo, normalmente llamará al otro como segunda opción.

El +operador siempre proporciona la pista Number, incluso si el primer operando es un valor de cadena. Aunque solicita xsu Numberrepresentación, dado que el primer operando devuelve una cadena de [[DefaultValue]], realiza una concatenación de cadenas.

Si desea garantizar que toStringse llame para la concatenación de cadenas, use una matriz y el .join("")método.

( +Sin embargo, ActionScript 3.0 modifica ligeramente el comportamiento de . Si cualquiera de los operandos es a String, lo tratará como un operador de concatenación de cadenas y usará la sugerencia Stringcuando llame [[DefaultValue]]. Por lo tanto, en AS3, este ejemplo produce "foo, x = foo, foo = x, foo1, 43, x = foo ".)

bcherry
fuente
1
También tenga en cuenta que si valueOfo toStringdevuelve no primitivos, se ignoran. Si ninguno existe, o ninguno devuelve un primitivo, TypeErrorse lanza a.
bcherry
1
Gracias bcherry, este es el calibre de respuesta que esperaba. Pero no debería x + "x ="; rendimiento "42x ="? Y x + "1"; rendimiento 421? Además, ¿tiene una URL para la parte relevante del estándar ECMAScript?
brainjam
2
En realidad, '+' no usa sugerencias (ver $ 11.6.1) por lo tanto, ToPrimitive invoca [[DefaultValue]](no-hint), que es equivalente a [[DefaultValue]](number).
user187291
9
Este no parece ser el caso de la clase Date incorporada. ("" + new Date(0)) === new Date(0).toString(). Un objeto Date siempre parece devolver su toString()valor cuando se agrega a algo.
kpozin
7
+1 y gracias! Encontré su publicación de blog en la que explica esta respuesta y quería vincularla / compartirla aquí. Fue realmente útil la adición a esta respuesta (incluido el comentario de Dmitry A. Soshnikov).
GitaarLAB
1

TLDR

La coerción de tipos, o conversión de tipos implícita, permite una escritura débil y se usa en todo JavaScript. La mayoría de los operadores (con la notable excepción de los operadores de igualdad estricta ===y !==), y las operaciones de verificación de valor (por ejemplo if(value)...), forzarán los valores que se les proporcionen, si los tipos de esos valores no son inmediatamente compatibles con la operación.

El mecanismo preciso utilizado para coaccionar un valor depende de la expresión que se evalúa. En la pregunta, se está utilizando el operador de suma .

El operador de suma primero se asegurará de que ambos operandos sean primitivos, lo que, en este caso, implica llamar al valueOfmétodo. El toStringmétodo no se llama en esta instancia porque el valueOfmétodo reemplazado en el objeto xdevuelve un valor primitivo.

Entonces, debido a que uno de los operandos en la pregunta es una cadena, ambos operandos se convierten en cadenas. Este proceso utiliza la operación interna abstracta ToString(nota: en mayúscula) y es distinto del toStringmétodo en el objeto (o su cadena prototipo).

Finalmente, las cadenas resultantes se concatenan.

Detalles

En el prototipo de cada objeto de función de constructor correspondiente a cada tipo de lenguaje en JavaScript (es decir, Número, BigInt, Cadena, Booleano, Símbolo y Objeto), hay dos métodos: valueOfy toString.

El propósito de valueOfes recuperar el valor primitivo asociado con un objeto (si tiene uno). Si un objeto no tiene un valor primitivo subyacente, simplemente se devuelve el objeto.

Si valueOfse invoca contra una primitiva, la primitiva se encuadra automáticamente de la forma normal y se devuelve el valor de la primitiva subyacente. Tenga en cuenta que para las cadenas, el valor primitivo subyacente (es decir, el valor devuelto por valueOf) es la propia representación de la cadena.

El siguiente código muestra que el valueOfmétodo devuelve el valor primitivo subyacente de un objeto contenedor, y muestra cómo las instancias de objetos no modificados que no corresponden a primitivas, no tienen ningún valor primitivo para devolver, por lo que simplemente regresan a sí mismas.

console.log(typeof new Boolean(true)) // 'object'
console.log(typeof new Boolean(true).valueOf()) // 'boolean'
console.log(({}).valueOf()) // {} (no primitive value to return)

El propósito de toString, por otro lado, es devolver una representación de cadena de un objeto.

Por ejemplo:

console.log({}.toString()) // '[object Object]'
console.log(new Number(1).toString()) // '1'

Para la mayoría de las operaciones, JavaScript intentará convertir silenciosamente uno o más operandos al tipo requerido. Este comportamiento se eligió para facilitar el uso de JavaScript. JavaScript inicialmente no tenía excepciones , y esto también puede haber jugado un papel en esta decisión de diseño. Este tipo de conversión de tipo implícita se denomina coerción de tipo y es la base del sistema de tipo flexible (débil) de JavaScript. Las complicadas reglas detrás de este comportamiento están destinadas a trasladar la complejidad del encasillamiento al lenguaje mismo y fuera de su código.

Durante el proceso coercitivo, hay dos modos de conversión que pueden ocurrir:

  1. Conversión de un objeto a primitivo (que podría implicar una conversión de tipo en sí), y
  2. La conversión directa a una instancia de tipo específico, el uso de un objeto de función constructor de uno de los tipos primitivos (es decir. Number(), Boolean(), String()Etc.)

Conversión a un primitivo

Cuando se intenta convertir tipos no primitivos en primitivos sobre los que operar, la operación abstracta ToPrimitivese llama con una "sugerencia" opcional de 'número' o 'cadena'. Si se omite la sugerencia, la sugerencia predeterminada es 'número' (a menos que el @@toPrimitivemétodo haya sido anulado). Si la pista es 'cadena', entonces toStringse intenta primero, y valueOfsegundo si toStringno devolvió una primitiva. De lo contrario, viceversa. La sugerencia depende de la operación que solicita la conversión.

El operador de adición no proporciona ninguna pista, por lo que valueOfse intenta primero. El operador de resta proporciona una pista de 'número', por lo que valueOfse intenta primero. Las únicas situaciones que puedo encontrar en la especificación en las que la pista es 'cadena' son:

  1. Object#toString
  2. La operación abstracta ToPropertyKey, que convierte un argumento en un valor que se puede utilizar como clave de propiedad.

Conversión de tipo directo

Cada operador tiene sus propias reglas para completar su operación. El operador de suma se utilizará primero ToPrimitivepara garantizar que cada operando sea una primitiva; luego, si cualquiera de los operandos es una cadena, entonces invocará deliberadamente la operación abstracta ToStringen cada operando, para entregar el comportamiento de concatenación de cadenas que esperamos con cadenas. Si, después del ToPrimitivepaso, ambos operandos no son cadenas, se realiza la suma aritmética.

A diferencia de la suma, el operador de resta no tiene un comportamiento sobrecargado, por lo que invocará toNumericen cada operando habiéndolos convertido primero en primitivas usando ToPrimitive.

Entonces:

 1  +  1   //  2                 
'1' +  1   // '11'   Both already primitives, RHS converted to string, '1' + '1',   '11'
 1  + [2]  // '12'   [2].valueOf() returns an object, so `toString` fallback is used, 1 + String([2]), '1' + '2', 12
 1  + {}   // '1[object Object]'    {}.valueOf() is not a primitive, so toString fallback used, String(1) + String({}), '1' + '[object Object]', '1[object Object]'
 2  - {}   // NaN    {}.valueOf() is not a primitive, so toString fallback used => 2 - Number('[object Object]'), NaN
+'a'       // NaN    `ToPrimitive` passed 'number' hint), Number('a'), NaN
+''        // 0      `ToPrimitive` passed 'number' hint), Number(''), 0
+'-1'      // -1     `ToPrimitive` passed 'number' hint), Number('-1'), -1
+{}        // NaN    `ToPrimitive` passed 'number' hint', `valueOf` returns an object, so falls back to `toString`, Number('[Object object]'), NaN
 1 + 'a'   // '1a'    Both are primitives, one is a string, String(1) + 'a'
 1 + {}    // '1[object Object]'    One primitive, one object, `ToPrimitive` passed no hint, meaning conversion to string will occur, one of the operands is now a string, String(1) + String({}), `1[object Object]`
[] + []    // ''     Two objects, `ToPrimitive` passed no hint, String([]) + String([]), '' (empty string)
 1 - 'a'   // NaN    Both are primitives, one is a string, `ToPrimitive` passed 'number' hint, 1-Number('a'), 1-NaN, NaN
 1 - {}    // NaN    One primitive, one is an object, `ToPrimitive` passed 'number' hint, `valueOf` returns object, so falls back to `toString`, 1-Number([object Object]), 1-NaN, NaN
[] - []    // 0      Two objects, `ToPrimitive` passed 'number' hint => `valueOf` returns array instance, so falls back to `toString`, Number('')-Number(''), 0-0, 0

Tenga en cuenta que el Dateobjeto intrínseco es único, ya que es el único intrínseco que anula el @@toPrimitivemétodo predeterminado , en el que se presume que la sugerencia predeterminada es 'cadena' (en lugar de 'número'). La razón para tener esto es que las Dateinstancias se traduzcan a cadenas legibles por defecto, en lugar de su valor numérico, para la conveniencia del programador. Puede anular @@toPrimitivesus propios objetos usando Symbol.toPrimitive.

La siguiente cuadrícula muestra los resultados de coerción para el operador de igualdad abstracto ( ==) ( fuente ):

ingrese la descripción de la imagen aquí

Vea también .

Ben Aston
fuente