What happends when object are added obj1 + obj2
, subtracted obj1 - obj2
or printed using alert(obj)
?
There are special methods in objects that do the conversion.
For object, there is no to-boolean conversion, because all objects are true
in a boolean context. So there are only string and numeric conversion.
When we output an object like alert(obj)
, it converts the object to string. String conversion happens usually in this situation.
The numeric conversion happens when we apply mathematical function. For instance, Date
object can be suntracted, and the result date1 - date2
is the time difference between two dates.
ToPrimitive
When an object is used in the context where a primitive is required, for instance, in an alert
or mathematical operations, it’s converted to a primitive values using the ToPrimiteve
algorithm.(specification)
That algorithm allows us to customize the conversion using a special object method.
Depending on the context, the conversion has a so-called “hint”.
There are three variants.
“string”
When an operation expects a string, for object-to-string conversion, like alert
:1
2// output
alert(obj);
“numeric”
When an operation expects a number, for object-to-number conversion, like maths:1
2
3
4
5
6
7
8// explicit conversion
let num = Number(obj);
// maths
let delta = date1 - date2;
// less/greater comparison between 2 objects
let greater = user1 > user2;
“default”
Occurs in rare case when the operator is “not sure” what type to expect.
For instance, binary plus +
can work both with strings (concatenates them) and numbers (adds them), so both strings and numbers would do. Or when an object is compared using ==
with a string, number or a symbol.1
2
3
4
5// binary plus
let total = car1 + car2;
// obj == string/number/symbol
if (user == 1) { ... };
The greater/less operator <> can work with both strings and numbers too. Still, it uses “number” hint, not “default”. That’s for historical reasons.
In practice, all built-in objects except for one case(Date
object) implement "default"
conversion the same way as "number"
. And probably we should do the same.
To do the conversion, JavaScript tries to call the three object methods:
- Call
Obj[Symbol.toPrimitive](hint)
if the method exists. - Otherwise if hint is
"string"
- try
obj.toString()
andobj.valueOf()
, whatever exists.
- try
- Otherwise if hint is
"number"
or"default"
- try
obj.valueOf()
andobj.toString()
, whatever exists.
- try
Symbol.toPrimitive
Let’s start from the first method. There’s a built-in symbol named Symbol.toPrimitive
(specification) that should be used to name the conversion method, like this:1
2
3
4obj[Symbol.toPrimitive] = function(hint) {
// return a primitive value
// hint = one of "string", "number", "default"
}
For instance, here user
object implements it:1
2
3
4
5
6
7
8
9
10
11
12
13
14let 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
As we can see from the code, the single method user[Symbol.toPrimitive]
handles all conversion cases. Depending on the conversion user
is converted to a string or a money amount.
toString/valueOf
Methods toString
and valueOf
provide an alternative “old-style” way to implement the conversion.
JavaScript tries to find those two method in the order:
toString -> valueOf
for"string"
hint.- ‘valueOf -> toString’ otherwise.
For instance, user
does the same thing as above using a combination of toString
and valueOf
.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19let 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
toPrimitive and toString/valueOf
The important thing to know about all primitive-conversion methods is that they do not necessarily return the “hinted” primitive.
There is no control whether toString() returns exactly a string, or whether Symbol.toPrimitive method returns a number for a hint “number”.
The only mandatory thing: these methods must return a primitive.
For instance:
Mathematical operations (except binary plus) perform
ToNumber
conversion:1
2
3
4
5
6
7let obj = {
toString() { // toString handles all conversions in the absence of other methods
return "2";
}
};
alert(obj * 2); // 4, ToPrimitive gives "2", then it becomes 2Binary plus checks the primitive – if it’s a string, then it does concatenation, otherwise it performs
ToNumber
and works with numbers.
string example:1
2
3
4
5
6
7let obj = {
toString() {
return "2";
}
};
alert(obj + 2); // 22 (ToPrimitive returned string => concatenation)
Number example:1
2
3
4
5
6
7let obj = {
toString() {
return true;
}
};
alert(obj + 2); // 3 (ToPrimitive returned boolean, not string => ToNumber)
Summary
The object-to-primitive conversion is called automatically by many built-in functions and operators that expect a primitive as a value.
There are 3 types (hints) of it:
"string"
(foralert
and other string conversion)"number"
(for maths)"default"
(few operators)
The specification describes explicitly which operator uses which hint. There are very few operators that “don’t know what to expect” and use the "default"
hint. Usually for built-in objects "default"
hint is handled the same way as "number"
, so in practice the last two are often merged together.
Reference: JavaScript.info