en.javascript.info/1-js/04-object-basics/05-object-toprimitive/article.md

238 lines
8.2 KiB
Markdown

# Object to primitive conversion
What happens when objects are added `obj1 + obj2`, subtracted `obj1 - obj2` or printed using `alert(obj)`?
There are special methods in objects that do the conversion.
In the chapter <info: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 close it.
[cut]
For objects, there's no to-boolean conversion, because all objects are `true` in a boolean context. So there are only string and numeric conversions.
The numeric conversion happens when we subtract objects or apply mathematical functions. For instance, `Date` objects (to be covered in the chapter <info:date>) can be subtracted, and the result of `date1 - date2` is the time difference between two dates.
As for the string conversion -- it usually happens when we output an object like `alert(obj)` and in similar contexts.
## 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 value using the `ToPrimitive` algorithm ([specification](https://tc39.github.io/ecma262/#sec-toprimitive)).
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 conversions, like `alert`:
```js
// output
alert(obj);
// using object as a property key
anotherObj[obj] = 123;
```
`"number"`
: When an operation expects a number, for object-to-number conversions, like maths:
```js
// 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;
```
`"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 both strings and numbers would do. Or when an object is compared using `==` with a string, number or a symbol.
```js
// 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, we'll learn it later) implement `"default"` conversion the same way as `"number"`. And probably we should do the same.
Please note -- there are only three hints. It's that simple. There is no "boolean" hint (all objects are `true` in boolean context) or anything else. And if we treat `"default"` and `"number"` the same, like most built-ins do, then there are only two conversions.
**To do the conversion, JavaScript tries to find and call three object methods:**
1. Call `obj[Symbol.toPrimitive](hint)` if the method exists,
2. Otherwise if hint is `"string"`
- try `obj.toString()` and `obj.valueOf()`, whatever exists.
3. Otherwise if hint is `"number"` or `"default"`
- try `obj.valueOf()` and `obj.toString()`, whatever exists.
## 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:
```js
obj[Symbol.toPrimitive] = function(hint) {
// return a primitive value
// hint = one of "string", "number", "default"
}
```
For instance, here `user` object implements it:
```js run
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
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.
If there's no `Symbol.toPrimitive` then JavaScript tries to find them and try in the order:
- `toString -> valueOf` for "string" hint.
- `valueOf -> toString` otherwise.
For instance, here `user` does the same as above using a combination of `toString` and `valueOf`:
```js run
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
```
Often we want a single "catch-all" place to handle all primitive conversions. In this case we can implement `toString` only, like this:
```js run
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.
## ToPrimitive and ToString/ToNumber
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.**
An operation that initiated the conversion gets that primitive, and then continues to work with it, applying further conversions if necessary.
For instance:
- Mathematical operations (except binary plus) perform `ToNumber` conversion:
```js run
let obj = {
toString() { // toString handles all conversions in the absence of other methods
return "2";
}
};
alert(obj * 2); // 4, ToPrimitive gives "2", then it becomes 2
```
- Binary plus checks the primitive -- if it's a string, then it does concatenation, otherwise it performs `ToNumber` and works with numbers.
String example:
```js run
let obj = {
toString() {
return "2";
}
};
alert(obj + 2); // 22 (ToPrimitive returned string => concatenation)
```
Number example:
```js run
let obj = {
toString() {
return true;
}
};
alert(obj + 2); // 3 (ToPrimitive returned boolean, not string => ToNumber)
```
```smart header="Historical notes"
For historical reasons, methods `toString` or `valueOf` *should* return a primitive: if any of them returns an object, then there's no error, but that object is ignored (like if the method didn't exist).
In contrast, `Symbol.toPrimitive` *must* return an primitive, otherwise there will be an error.
```
## 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"` (for `alert` and other string conversions)
- `"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.
The conversion algorithm is:
1. Call `obj[Symbol.toPrimitive](hint)` if the method exists,
2. Otherwise if hint is `"string"`
- try `obj.toString()` and `obj.valueOf()`, whatever exists.
3. Otherwise if hint is `"number"` or `"default"`
- try `obj.valueOf()` and `obj.toString()`, whatever exists.
In practice, it's often enough to implement only `obj.toString()` as a "catch-all" method for all conversions that return a "human-readable" representation of an object, for logging or debugging purposes.