tags:

views:

239

answers:

2

In Javascript every object has a valueOf() and toString() method. I would have thought that the toString() method got invoked whenever a string conversion is called for, but apparently it is trumped by valueOf().

For example, the code

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

will print

x=42
x=foo

This strikes me as backwards .. if x were a complex number, for example, I would want valueOf() to give me its magnitude (so that zero would become special), but whenever I wanted to convert to a string I would want something like "a+bi". And I wouldn't want to have to call toString() explicitly in contexts that implied a string.

Is this just the way it is?

+3  A: 

Here's a little more detail, before I get to the answer:

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"

The toString function is not "trumped" by valueOf in general. The ECMAScript standard actually answers this question pretty well. Every object has a [[DefaultValue]] property, which is computed on-demand. When asking for this property, the interpreter also provides a "hint" for what sort of value it expects. If the hint is String, then toString is used before valueOf. But, if the hint is Number, then valueOf will be used first. Note that if only one is present, or it returns a non-primitive, it will usually call the other as the second choice.

The + operator always provides the hint Number, even if the first operand is a string value. Even though it asks x for its Number representation, since the first operand returns a string from [[DefaultValue]], it does string concatenation.

If you want to guarantee that toString is called for string concatenation, use an array and the .join("") method.

bcherry
Also note that if `valueOf` or `toString` return non-primitives, they are ignored. If neither exists, or neither returns a primitive, then a `TypeError` is thrown.
bcherry
Thanks bcherry, this is the calibre of answer I was hoping for.But shouldn't x + "x="; yield "42x=" ?And x + "1"; yield 421 ?Also, do you have a URL for the relevant part of the the ECMAScript standard?
brainjam
Oops, yeah. A typo and a mistake from testing `"1" + 1`, respectively. I've edited the original post. Thanks :) Here is the ECMAScript 3 specification (pdf): http://www.mozilla.org/js/language/E262-3.pdf You're looking for `8.6.2.6 [[DefaultValue]] (hint)`, which is on page 35.
bcherry
Actually, '+' doesn't use hints (see $11.6.1) therefore ToPrimitive invokes `[[DefaultValue]](no-hint)`, which is equivalent to `[[DefaultValue]](number)`.
stereofrog
Oh, interesting, stereofrog. I didn't see anything about hints and `+`, but assumed it was using `Number` based on the result.I also noticed that the unary `+` uses the `Number` hint (or no hint at all), but then calls `ToNumber` on the result. This means that if `valueOf` returned a string, using unary `+` on that would return `NaN`, not the string, which is what I had expected.
bcherry
+2  A: 

The reason why ("x="+x) gives "x=value" and not "x=tostring" is the following. When evaluating "+", javascript first collects primitive values of the operands, and then decides if addition or concatenation should be applied, based on the type of each primitive.

So, this is how you think it works

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

and this is what actually happens

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

That is, toString is applied to the result of valueOf, not to your original object.

For further reference, check out http://www.mozilla.org/js/language/E262-3.pdf $11.6.1


*When called in string context, ToPrimitive does invoke toString, but this is not the case here, because '+' doesn't enforce any type context.

stereofrog
brainjam
The standard definitely says "or" (see the link).
stereofrog
Yes, you're right.
brainjam