What happens when objects are added obj1 + obj2, subtracted obj1 - obj2 or printed using alert(obj)?
JavaScript doesnât allow you to customize how operators work on objects. Unlike some other programming languages, such as Ruby or C++, we canât implement a special object method to handle addition (or other operators).
In case of such operations, objects are auto-converted to primitives, and then the operation is carried out over these primitives and results in a primitive value.
Thatâs an important limitation: the result of obj1 + obj2 (or another math operation) canât be another object!
E.g. we canât make objects representing vectors or matrices (or achievements or whatever), add them and expect a âsummedâ object as the result. Such architectural feats are automatically âoff the boardâ.
So, because we canât technically do much here, thereâs no maths with objects in real projects. When it happens, with rare exceptions, itâs because of a coding mistake.
In this chapter weâll cover how an object converts to primitive and how to customize it.
We have two purposes:
- It will allow us to understand whatâs going on in case of coding mistakes, when such an operation happened accidentally.
- There are exceptions, where such operations are possible and look good. E.g. subtracting or comparing dates (
Dateobjects). Weâll come across them later.
Conversion rules
In the chapter Type Conversions weâve seen the rules for numeric, string and boolean conversions of primitives. But we left a gap for objects. Now, as we know about methods and symbols it becomes possible to fill it.
- Thereâs no conversion to boolean. All objects are
truein a boolean context, as simple as that. There exist only numeric and string conversions. - The numeric conversion happens when we subtract objects or apply mathematical functions. For instance,
Dateobjects (to be covered in the chapter Date and time) can be subtracted, and the result ofdate1 - date2is the time difference between two dates. - As for the string conversion â it usually happens when we output an object with
alert(obj)and in similar contexts.
We can implement string and numeric conversion by ourselves, using special object methods.
Now letâs get into technical details, because itâs the only way to cover the topic in-depth.
Hints
How does JavaScript decide which conversion to apply?
There are three variants of type conversion, that happen in various situations. Theyâre called âhintsâ, as described in the specification:
"string"-
For an object-to-string conversion, when weâre doing an operation on an object that expects a string, like
alert:// output alert(obj); // using object as a property key anotherObj[obj] = 123; "number"-
For an object-to-number conversion, like when weâre doing maths:
// explicit conversion let num = Number(obj); // maths (except binary plus) let n = +obj; // unary plus let delta = date1 - date2; // less/greater comparison let greater = user1 > user2;Most built-in mathematical functions also include such conversion.
"default"-
Occurs in rare cases 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 if a binary plus gets an object as an argument, it uses the"default"hint to convert it.Also, if an object is compared using
==with a string, number or a symbol, itâs also unclear which conversion should be done, so the"default"hint is used.// binary plus uses the "default" hint let total = obj1 + obj2; // obj == number uses the "default" hint if (user == 1) { ... };The greater and less comparison operators, such as
<>, can work with both strings and numbers too. Still, they use the"number"hint, not"default". Thatâs for historical reasons.
In practice though, things are a bit simpler.
All built-in objects except for one case (Date object, weâll learn it later) implement "default" conversion the same way as "number". And we probably should do the same.
Still, itâs important to know about all 3 hints, soon weâll see why.
To do the conversion, JavaScript tries to find and call three object methods:
- Call
obj[Symbol.toPrimitive](hint)â the method with the symbolic keySymbol.toPrimitive(system symbol), if such method exists, - Otherwise if hint is
"string"- try calling
obj.toString()orobj.valueOf(), whatever exists.
- try calling
- Otherwise if hint is
"number"or"default"- try calling
obj.valueOf()orobj.toString(), whatever exists.
- try calling
Symbol.toPrimitive
Letâs start from the first method. Thereâs a built-in symbol named Symbol.toPrimitive that should be used to name the conversion method, like this:
obj[Symbol.toPrimitive] = function(hint) {
// here goes the code to convert this object to a primitive
// it must return a primitive value
// hint = one of "string", "number", "default"
};
If the method Symbol.toPrimitive exists, itâs used for all hints, and no more methods are needed.
For instance, here user object implements it:
let 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, user becomes a self-descriptive string or a money amount, depending on the conversion. The single method user[Symbol.toPrimitive] handles all conversion cases.
toString/valueOf
If thereâs no Symbol.toPrimitive then JavaScript tries to find methods toString and valueOf:
- For the
"string"hint: calltoStringmethod, and if it doesnât exist or if it returns an object instead of a primitive value, then callvalueOf(sotoStringhas the priority for string conversions). - For other hints: call
valueOf, and if it doesnât exist or if it returns an object instead of a primitive value, then calltoString(sovalueOfhas the priority for maths).
Methods toString and valueOf come from ancient times. They are not symbols (symbols did not exist that long ago), but rather âregularâ string-named methods. They provide an alternative âold-styleâ way to implement the conversion.
These methods must return a primitive value. If toString or valueOf returns an object, then itâs ignored (same as if there were no method).
By default, a plain object has following toString and valueOf methods:
- The
toStringmethod returns a string"[object Object]". - The
valueOfmethod returns the object itself.
Hereâs the demo:
let user = {name: "John"};
alert(user); // [object Object]
alert(user.valueOf() === user); // true
So if we try to use an object as a string, like in an alert or so, then by default we see [object Object].
The default valueOf is mentioned here only for the sake of completeness, to avoid any confusion. As you can see, it returns the object itself, and so is ignored. Donât ask me why, thatâs for historical reasons. So we can assume it doesnât exist.
Letâs implement these methods to customize the conversion.
For instance, here user does the same as above using a combination of toString and valueOf instead of Symbol.toPrimitive:
let 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
As we can see, the behavior is the same as the previous example with Symbol.toPrimitive.
Often we want a single âcatch-allâ place to handle all primitive conversions. In this case, we can implement toString only, like this:
let user = {
name: "John",
toString() {
return this.name;
}
};
alert(user); // toString -> John
alert(user + 500); // toString -> John500
In the absence of Symbol.toPrimitive and valueOf, toString will handle all primitive conversions.
A conversion can return any primitive type
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 the hint "number".
The only mandatory thing: these methods must return a primitive, not an object.
For historical reasons, if toString or valueOf returns an object, thereâs no error, but such value is ignored (like if the method didnât exist). Thatâs because in ancient times there was no good âerrorâ concept in JavaScript.
In contrast, Symbol.toPrimitive is stricter, it must return a primitive, otherwise there will be an error.
Further conversions
As we know already, many operators and functions perform type conversions, e.g. multiplication * converts operands to numbers.
If we pass an object as an argument, then there are two stages of calculations:
- The object is converted to a primitive (using the rules described above).
- If necessary for further calculations, the resulting primitive is also converted.
For instance:
let obj = {
// toString handles all conversions in the absence of other methods
toString() {
return "2";
}
};
alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number
- The multiplication
obj * 2first converts the object to primitive (thatâs a string"2"). - Then
"2" * 2becomes2 * 2(the string is converted to number).
Binary plus will concatenate strings in the same situation, as it gladly accepts a string:
let obj = {
toString() {
return "2";
}
};
alert(obj + 2); // "22" ("2" + 2), conversion to primitive returned a string => concatenation
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"(foralertand other operations that need a string)"number"(for maths)"default"(few operators, usually objects implement it the same way as"number")
The specification describes explicitly which operator uses which hint.
The conversion algorithm is:
- Call
obj[Symbol.toPrimitive](hint)if the method exists, - Otherwise if hint is
"string"- try calling
obj.toString()orobj.valueOf(), whatever exists.
- try calling
- Otherwise if hint is
"number"or"default"- try calling
obj.valueOf()orobj.toString(), whatever exists.
- try calling
All these methods must return a primitive to work (if defined).
In practice, itâs often enough to implement only obj.toString() as a âcatch-allâ method for string conversions that should return a âhuman-readableâ representation of an object, for logging or debugging purposes.
Comments
<code>tag, for several lines â wrap them in<pre>tag, for more than 10 lines â use a sandbox (plnkr, jsbin, codepenâ¦)