valueOf () vs. toString () em Javascript

115

Em Javascript, todo objeto tem um método valueOf () e toString (). Eu teria pensado que o método toString () fosse invocado sempre que uma conversão de string fosse solicitada, mas aparentemente ele é superado por valueOf ().

Por exemplo, o código

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

irá imprimir

x=42
x=foo

Isso me parece estar ao contrário .. se x fosse um número complexo, por exemplo, eu gostaria que valueOf () me desse sua magnitude, mas sempre que eu quisesse converter para uma string, eu gostaria de algo como "a + bi". E eu não gostaria de ter que chamar toString () explicitamente em contextos que impliquem uma string.

É assim que as coisas são?

cérebro
fonte
6
Você já tentou window.console.log (x);ou alert (x);?
Li0liQ
5
Eles fornecem "Object" e "foo" respectivamente. Coisas divertidas.
brainjam
Na verdade, alert (x); fornece "foo" e window.console.log (x); fornece "foo {}" no Firebug e o objeto inteiro no console do Chrome.
brainjam
No Firefox 33.0.2 é alert(x)exibido fooe window.console.log(x)exibido Object { toString: x.toString(), valueOf: x.valueOf() }.
John Sonderson

Respostas:

107

A razão pela qual ("x =" + x) dá "x = valor" e não "x = tostring" é a seguinte. Ao avaliar "+", o javascript primeiro coleta os valores primitivos dos operandos e, em seguida, decide se a adição ou concatenação deve ser aplicada, com base no tipo de cada primitiva.

Então, é assim que você acha que funciona

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

e isso é o que realmente acontece

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))

Ou seja, toString é aplicado ao resultado de valueOf, não ao seu objeto original.

Para obter mais referências, consulte a seção 11.6.1 O operador Addition (+) na Especificação de linguagem ECMAScript.


* Quando chamado em um contexto string, ToPrimitive faz invocar toString, mas este não é o caso aqui, porque '+' não impõe qualquer contexto tipo.

user187291
fonte
3
A condicional no bloco "na verdade" não deveria ser lida "if (pa é string && pb é string)"? Ou seja, "&&" em vez de "||" ?
brainjam
3
O padrão definitivamente diz "ou" (veja o link).
user187291
2
Sim, isso é exatamente correto - a precedência é dada a strings sobre outros tipos na concatenação. Se qualquer um dos operandos for uma string, tudo será concatenado como uma string. Boa resposta.
devios1
76

Aqui estão mais alguns detalhes, antes de eu chegar à resposta:

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"

A toStringfunção não é "superada" pelo valueOfgeral. O padrão ECMAScript responde muito bem a essa pergunta. Cada objeto possui uma [[DefaultValue]]propriedade, que é calculada sob demanda. Ao solicitar essa propriedade, o interpretador também fornece uma "dica" do tipo de valor que ele espera. Se a dica for String, então toStringserá usado antes valueOf. Mas, se a dica for Number, então valueOfserá usada primeiro. Observe que se apenas um estiver presente ou se ele retornar um não primitivo, ele geralmente chamará o outro como a segunda escolha.

O +operador sempre fornece a dica Number, mesmo se o primeiro operando for um valor de string. Mesmo que peça xsua Numberrepresentação, como o primeiro operando retorna uma string de [[DefaultValue]], ele faz a concatenação de strings.

Se você quiser garantir que toStringseja chamado para concatenação de string, use uma matriz e o .join("")método.

(O ActionScript 3.0 modifica ligeiramente o comportamento de +, no entanto. Se um dos operandos for a String, ele o tratará como um operador de concatenação de string e usará a dica Stringao chamar [[DefaultValue]]. Portanto, no AS3, este exemplo produz "foo, x = foo, foo = x, foo1, 43, x = foo ".)

bcherry
fonte
1
Observe também que se valueOfou toStringretornar não-primitivos, eles serão ignorados. Se nenhum deles existir ou se nenhum retornar um primitivo, a TypeErrorserá lançado.
bcherry
1
Obrigado bcherry, este é o calibre de resposta que eu esperava. Mas não deveria x + "x ="; rendimento "42x ="? E x + "1"; rendimento 421? Além disso, você tem um URL para a parte relevante do padrão ECMAScript?
brainjam
2
Na verdade, '+' não usa dicas (consulte $ 11.6.1), portanto, ToPrimitive invoca [[DefaultValue]](no-hint), que é equivalente a [[DefaultValue]](number).
user187291
9
Este não parece ser o caso da classe interna Date. ("" + new Date(0)) === new Date(0).toString(). Um objeto Date sempre parece retornar seu toString()valor quando é adicionado a algo.
kpozin de
7
+1 e Thx! Eu encontrei sua postagem no blog na qual você elaborou esta resposta e queria um link / compartilhar aqui. Foi realmente uma adição útil a esta resposta (incluindo o comentário de Dmitry A. Soshnikov).
GitaarLAB de
1

TLDR

A coerção de tipo, ou conversão de tipo implícita, permite a digitação fraca e é usada em todo o JavaScript. A maioria dos operadores (com a notável exceção dos operadores de igualdade estrita ===e !==) e operações de verificação de valor (por exemplo if(value)...), forçará os valores fornecidos a eles, se os tipos desses valores não forem imediatamente compatíveis com a operação.

O mecanismo preciso usado para coagir um valor depende da expressão que está sendo avaliada. Na pergunta, o operador de adição está sendo usado.

O operador de adição primeiro garantirá que ambos os operandos sejam primitivos, o que, neste caso, envolve a chamada do valueOfmétodo. O toStringmétodo não é chamado nesta instância porque o valueOfmétodo sobrescrito no objeto xretorna um valor primitivo.

Então, como um dos operandos em questão é uma string, ambos os operandos são convertidos em strings. Esse processo usa a operação interna abstrata ToString(nota: letras maiúsculas) e é diferente do toStringmétodo no objeto (ou de sua cadeia de protótipo).

Finalmente, as strings resultantes são concatenadas.

Detalhes

No protótipo de cada objeto de função do construtor correspondente a cada tipo de linguagem em JavaScript (ou seja, Number, BigInt, String, Boolean, Symbol e Object), existem dois métodos: valueOfe toString.

O objetivo de valueOfé recuperar o valor primitivo associado a um objeto (se houver). Se um objeto não tiver um valor primitivo subjacente, o objeto será simplesmente retornado.

Se valueOffor invocado contra um primitivo, então o primitivo é automaticamente encaixotado da maneira normal e o valor primitivo subjacente é retornado. Observe que, para strings, o valor primitivo subjacente (ou seja, o valor retornado por valueOf) é a própria representação da string.

O código a seguir mostra que o valueOfmétodo retorna o valor primitivo subjacente de um objeto wrapper e mostra como as instâncias de objeto não modificadas que não correspondem aos primitivos não têm valor primitivo para retornar, portanto, simplesmente retornam a si mesmas.

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

O objetivo de toString, por outro lado, é retornar uma representação em string de um objeto.

Por exemplo:

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

Para a maioria das operações, o JavaScript tentará silenciosamente converter um ou mais operando (s) para o tipo necessário. Esse comportamento foi escolhido para tornar o JavaScript mais fácil de usar. JavaScript inicialmente não tinha exceções e isso também pode ter desempenhado um papel nesta decisão de design. Esse tipo de conversão implícita de tipo é chamado de coerção de tipo e é a base do sistema de tipo flexível (fraco) do JavaScript. As regras complicadas por trás desse comportamento têm o objetivo de mover a complexidade da conversão de tipos para a própria linguagem e para fora do seu código.

Durante o processo coercitivo, existem dois modos de conversão que podem ocorrer:

  1. Conversão de um objeto em um primitivo (que pode envolver uma conversão de tipo em si), e
  2. Conversão direta para uma instância de tipo específico, usando um objeto de função de construtor de um dos tipos primitivos (ou seja Number(), . Boolean(), String()Etc.)

Conversão para um primitivo

Ao tentar converter tipos não primitivos em primitivos a serem operados, a operação abstrata ToPrimitiveé chamada com uma "dica" opcional de 'número' ou 'string'. Se a dica for omitida, a dica padrão é 'número' (a menos que o @@toPrimitivemétodo tenha sido substituído). Se a dica for 'string', ela toStringserá tentada primeiro e , em seguida , valueOfse toStringnão retornar um primitivo. Caso contrário, vice-versa. A dica depende da operação que solicita a conversão.

O operador de adição não fornece nenhuma dica, então valueOfé tentado primeiro. O operador de subtração fornece uma dica de 'número', então valueOfé tentado primeiro. As únicas situações que posso encontrar na especificação em que a dica é 'string' são:

  1. Object#toString
  2. A operação abstrata ToPropertyKey, que converte um argumento em um valor que pode ser usado como uma chave de propriedade

Conversão direta de tipo

Cada operador tem suas próprias regras para completar sua operação. O operador de adição será usado primeiro ToPrimitivepara garantir que cada operando seja um primitivo; então, se qualquer operando for uma string, ele invocará deliberadamente a operação abstrata ToStringem cada operando, para entregar o comportamento de concatenação de string que esperamos com as strings. Se, após a ToPrimitiveetapa, ambos os operandos não forem strings, então a adição aritmética é executada.

Ao contrário da adição, o operador de subtração não tem comportamento sobrecarregado e, portanto, invocará toNumericcada operando tendo primeiro os convertido em primitivos usando ToPrimitive.

Assim:

 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

Observe que o Dateobjeto intrínseco é único, pois é o único intrínseco a substituir o @@toPrimitivemétodo padrão , no qual a dica padrão é presumida como 'string' (em vez de 'número'). A razão para isso é que as Dateinstâncias sejam convertidas em strings legíveis por padrão, em vez de seu valor numérico, para a conveniência do programador. Você pode substituir @@toPrimitiveem seus próprios objetos usando Symbol.toPrimitive.

A grade a seguir mostra os resultados da coerção para o operador de igualdade abstrato ( ==) ( fonte ):

insira a descrição da imagem aqui

Veja também .

Ben Aston
fonte