Skip to content

Commit a5c6e19

Browse files
committed
add Object to primitive conversion
1 parent 55238b1 commit a5c6e19

File tree

2 files changed

+253
-1
lines changed

2 files changed

+253
-1
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,6 @@ We concentrate on the language itself here, with the minimum of environment-spec
7272

7373
4.4 [Object methods, "this"](pages/4.4-Object-methods.md)
7474

75-
4.5 Object to primitive conversion
75+
4.5 [Object to primitive conversion](pages/4.5-Object-to-primitive-conversion.md)
7676

7777
4.6 Constructor, operator "new"
+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Object to primitive conversion
2+
3+
What happens when objects are added `obj1 + obj2`, subtracted `obj1 - obj2` or printed using `alert(obj)`?
4+
5+
There are special methods in objects that do the conversion.
6+
7+
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 close it.
8+
9+
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.
10+
11+
The numeric conversion happens when we subtract objects or apply mathematical functions. For instance, Date objects (to be covered in the chapter Date and time) can be subtracted, and the result of date1 - date2 is the time difference between two dates.
12+
13+
As for the string conversion – it usually happens when we output an object like `alert(obj)` and in similar contexts.
14+
15+
## ToPrimitive
16+
17+
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).
18+
19+
That algorithm allows us to customize the conversion using a special object method.
20+
21+
Depending on the context, the conversion has a so-called “hint”.
22+
23+
There are three variants:
24+
25+
`"string"`
26+
27+
When an operation expects a string, for object-to-string conversions, like `alert`:
28+
29+
```
30+
// output
31+
alert(obj);
32+
33+
// using object as a property key
34+
anotherObj[obj] = 123;
35+
```
36+
37+
`"number"`
38+
39+
When an operation expects a number, for object-to-number conversions, like maths:
40+
41+
```
42+
// explicit conversion
43+
let num = Number(obj);
44+
45+
// maths (except binary plus)
46+
let n = +obj; // unary plus
47+
let delta = date1 - date2;
48+
49+
// less/greater comparison
50+
let greater = user1 > user2;
51+
```
52+
53+
***`"default"`***
54+
55+
Occurs in rare cases when the operator is “not sure” what type to expect.
56+
57+
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.
58+
59+
```
60+
// binary plus
61+
let total = car1 + car2;
62+
63+
// obj == string/number/symbol
64+
if (user == 1) { ... };
65+
```
66+
67+
The greater/less operator `<>` can work with both strings and numbers too. Still, it uses “number” hint, not “default”. That’s for historical reasons.
68+
69+
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.
70+
71+
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.
72+
73+
***To do the conversion, JavaScript tries to find and call three object methods:***
74+
75+
1. Call `obj[Symbol.toPrimitive](hint)` if the method exists,
76+
77+
2. Otherwise if hint is "string"
78+
* try `obj.toString()` and `obj.valueOf()`, whatever exists.
79+
80+
3. Otherwise if hint is `"number"` or `"default"`
81+
* try `obj.valueOf()` and `obj.toString()`, whatever exists.
82+
83+
## Symbol.toPrimitive
84+
85+
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:
86+
87+
```
88+
obj[Symbol.toPrimitive] = function(hint) {
89+
// return a primitive value
90+
// hint = one of "string", "number", "default"
91+
}
92+
```
93+
94+
For instance, here `user` object implements it:
95+
96+
```
97+
let user = {
98+
name: "John",
99+
money: 1000,
100+
101+
[Symbol.toPrimitive](hint) {
102+
alert(`hint: ${hint}`);
103+
return hint == "string" ? `{name: "${this.name}"}` : this.money;
104+
}
105+
};
106+
107+
// conversions demo:
108+
alert(user); // hint: string -> {name: "John"}
109+
alert(+user); // hint: number -> 1000
110+
alert(user + 500); // hint: default -> 1500
111+
```
112+
113+
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.
114+
115+
## toString/valueOf
116+
117+
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.
118+
119+
If there’s no `Symbol.toPrimitive` then JavaScript tries to find them and try in the order:
120+
121+
* `toString -> valueOf` for “string” hint.
122+
123+
* `valueOf -> toString` otherwise.
124+
125+
For instance, here `user` does the same as above using a combination of `toString` and `valueOf`:
126+
127+
```
128+
let user = {
129+
name: "John",
130+
money: 1000,
131+
132+
// for hint="string"
133+
toString() {
134+
return `{name: "${this.name}"}`;
135+
},
136+
137+
// for hint="number" or "default"
138+
valueOf() {
139+
return this.money;
140+
}
141+
142+
};
143+
144+
alert(user); // toString -> {name: "John"}
145+
alert(+user); // valueOf -> 1000
146+
alert(user + 500); // valueOf -> 1500
147+
```
148+
149+
Often we want a single “catch-all” place to handle all primitive conversions. In this case we can implement `toString` only, like this:
150+
151+
```
152+
let user = {
153+
name: "John",
154+
155+
toString() {
156+
return this.name;
157+
}
158+
};
159+
160+
alert(user); // toString -> John
161+
alert(user + 500); // toString -> John500
162+
```
163+
164+
In the absence of `Symbol.toPrimitive` and `valueOf`, `toString` will handle all primitive conversions.
165+
166+
## ToPrimitive and ToString/ToNumber
167+
168+
The important thing to know about all primitive-conversion methods is that they do not necessarily return the “hinted” primitive.
169+
170+
There is no control whether `toString()` returns exactly a string, or whether `Symbol.toPrimitive` method returns a number for a hint “number”.
171+
172+
***The only mandatory thing: these methods must return a primitive.***
173+
174+
An operation that initiated the conversion gets that primitive, and then continues to work with it, applying further conversions if necessary.
175+
176+
For instance:
177+
178+
* Mathematical operations (except binary plus) perform `ToNumber` conversion:
179+
180+
```
181+
let obj = {
182+
toString() { // toString handles all conversions in the absence of other methods
183+
return "2";
184+
}
185+
};
186+
187+
alert(obj * 2); // 4, ToPrimitive gives "2", then it becomes 2
188+
```
189+
190+
* Binary plus checks the primitive – if it’s a string, then it does concatenation, otherwise it performs ToNumber and works with numbers.
191+
192+
String example:
193+
194+
```
195+
let obj = {
196+
toString() {
197+
return "2";
198+
}
199+
};
200+
201+
alert(obj + 2); // 22 (ToPrimitive returned string => concatenation)
202+
```
203+
204+
Number example:
205+
206+
```
207+
let obj = {
208+
toString() {
209+
return true;
210+
}
211+
};
212+
213+
alert(obj + 2); // 3 (ToPrimitive returned boolean, not string => ToNumber)
214+
```
215+
216+
***
217+
218+
#### Historical notes
219+
220+
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).
221+
222+
In contrast, `Symbol.toPrimitive` must return a primitive, otherwise, there will be an error.
223+
224+
***
225+
226+
## Summary
227+
228+
The object-to-primitive conversion is called automatically by many built-in functions and operators that expect a primitive as a value.
229+
230+
There are 3 types (hints) of it:
231+
232+
* `"string"` (for `alert` and other string conversions)
233+
234+
* `"number"` (for maths)
235+
236+
* `"default"` (few operators)
237+
238+
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.
239+
240+
The conversion algorithm is:
241+
242+
1. Call `obj[Symbol.toPrimitive](hint)` if the method exists,
243+
244+
2. Otherwise if hint is `"string"`
245+
246+
* try `obj.toString()` and `obj.valueOf()`, whatever exists.
247+
248+
3. Otherwise if hint is `"number"` or `"default"`
249+
250+
* try `obj.valueOf()` and `obj.toString()`, whatever exists.
251+
252+
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.

0 commit comments

Comments
 (0)