You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -5,134 +5,178 @@ In the chapter <info:type-conversions> we've seen the rules for numeric, string
5
5
6
6
But we left a gap for objects. Now let's fill it.
7
7
8
+
8
9
[cut]
9
10
10
-
For objects, there's a special additional conversion called [ToPrimitive](https://tc39.github.io/ecma262/#sec-toprimitive).
11
+
## Where and why?
11
12
12
-
For some built-in objects it is implemented in special way, but mostly comes in two flavors:
13
+
The process of object to primitive conversion can be customized, here we'll see how to implement our own methods for it.
13
14
14
-
-`ToPrimitive(obj, "string")` for a conversion to string
15
-
-`ToPrimitive(obj, "number")` for a conversion to number
15
+
But first, let's note that conversion of an object to primitive value (a number or a string) is a rare thing in practice.
16
16
17
-
So, if we convert an object to string, then first `ToPrimitive(obj, "string")` is applied, and then the resulting primitive is converted using primitive rules. The similar thing for a numeric conversion.
17
+
Just think about cases when such conversion happens. For numeric conversion, when we compare an object against a primitive: `user == 18`. But what do we mean here? Maybe to compare the user's age? Then wouldn't it be more obvious to write `user.age == 18`? And read it later too.
18
18
19
-
What's most interesting in `ToPrimitive`is its customizability.
19
+
Or, for a string conversion... Where does it happen? Usually, when we output an object. But simple ways of output like `alert(user)` are only used for debugging and logging purposes. In projects, the output is more complicated. And it may require additional parameters too, so it should be implemented separately, maybe with methods.
20
20
21
-
## toString and valueOf
21
+
Most of the time, it's more flexible and gives more readable code to explicitly write an object property or call a method than rely on the conversion.
22
22
23
-
`ToPrimitive` is customizable via methods `toString()` and `valueOf()`.
23
+
That said, there are still valid reasons why we should know how it works.
24
24
25
-
The general algorithm of `ToPrimitive(obj, "string")` is:
25
+
- The `alert(user)` kind of output is still used for logging and debugging.
26
+
- The built-in `toString` method of objects allows to get the type of almost anything.
27
+
- Sometimes it just happens (on mistake?), and we should understand what's going on.
26
28
27
29
28
-
1. Call the method `obj.toString()` if it exists.
29
-
2. If the result is a primitive, return it.
30
-
3. Call the method `obj.valueOf()` if it exists.
31
-
4. If the result is a primitive, return it.
32
-
5. Otherwise `TypeError` (conversion failed)
30
+
## ToPrimitive
33
31
32
+
When an object is used as a primitive, the special internal algorithm named [ToPrimitive](https://tc39.github.io/ecma262/#sec-toprimitive) is invoked.
34
33
35
-
The `ToPrimitive(obj, "number")` is the same, but `valueOf()` and `toString()` are swapped:
34
+
It comes in 3 flavours:
36
35
37
-
1. Call the method `obj.valueOf()` if it exists.
38
-
2. If the result is a primitive, return it.
39
-
3. Call the method `obj.toString()` if it exists.
40
-
4. If the result is a primitive, return it.
41
-
5. Otherwise `TypeError` (conversion failed)
36
+
-`ToPrimitive(obj, "string")`
37
+
-`ToPrimitive(obj, "number")`
38
+
-`ToPrimitive(obj)`, the so-called "default" flavour
42
39
43
-
```smart header="ToPrimitive returns a primitive, but its type is not guaranteed"
44
-
As we can see, the result of `ToPrimitive` is always a primitive, because even if `toString/valueOf` return a non-primitive value, it is ignored.
40
+
When [ToString](https://tc39.github.io/ecma262/#sec-tostring) conversion is applied to objects, it does two steps:
45
41
46
-
But it can be any primitive. There's no control whether `toString()` returns exactly a string or, say a boolean.
47
-
```
48
42
49
-
Let's see an example. Here we implement our own string conversion for `user`:
43
+
1.`let primitive = ToPrimitive(obj, "string")`
44
+
2.`return ToString(primitive)`
45
+
46
+
In other words, `ToPrimitive` is applied first, then the result (a primitive) is converted to string using [primitive rules](info:type-conversions) that we already know.
47
+
48
+
When [ToNumber](https://tc39.github.io/ecma262/#sec-tonumber) conversion is applied to objects, it also does two similar steps:
49
+
50
+
1.`let primitive = ToPrimitive(obj, "number")`
51
+
2.`return ToNumber(primitive)`
52
+
53
+
Again, `ToPrimitive` is applied first, then the primitive result is converted to number.
54
+
55
+
The "default" flavour occurs in [binary `+`](https://tc39.github.io/ecma262/#sec-addition-operator-plus-runtime-semantics-evaluation) operator, in [equality test](https://tc39.github.io/ecma262/#sec-abstract-equality-comparison) of an object versus a string, a number or a symbol, and in few other rare cases. It exists mainly for historical reasons to cover few backwards-compatible edge cases. Most of time it does that same as "number", but we should be aware of this case if we're impementing our own conversion method.
56
+
57
+
So, to understand `ToNumber` and `ToString` for objects, we should redirect ourselves to `ToPrimitive`.
58
+
59
+
## The new style: Symbol.toPrimitive
60
+
61
+
The internal `ToPrimitive(obj, hint)` call has two parameters:
62
+
63
+
`obj`
64
+
: The object to transform.
65
+
66
+
`hint`
67
+
: The flavour: one of `"string"`, `"number"` or `"default"`.
68
+
69
+
If the object has `Symbol.toPrimitive` method implemented, which it is called with the `hint`.
70
+
71
+
For instance:
50
72
51
73
```js run
52
74
let user = {
75
+
name:"John",
76
+
age:30,
53
77
54
-
name:'John',
55
-
56
-
*!*
57
-
toString() {
58
-
return`User ${this.firstName}`;
78
+
// must return a primitive
79
+
[Symbol.toPrimitive](hint) {
80
+
alert(`hint: ${hint}`);
81
+
return hint =="string"?this.name:this.age;
59
82
}
60
-
*/!*
61
-
83
+
62
84
};
63
85
64
-
*!*
65
-
alert( user ); // User John
66
-
*/!*
86
+
// conversions demo:
87
+
alert(user); // hint: string -> John
88
+
alert(+user); // hint: number -> 30
89
+
alert(user +1); // hint: default -> 31
67
90
```
68
91
69
-
Looks much better than the default `[object Object]`, right?
70
92
93
+
For modern scripts, `Symbol.toPrimitive` can be enough. Two other methods: `toString` and `valueOf` exist for historical reasons and for backwards compatibility.
94
+
95
+
96
+
<!--
97
+
The algorithm basically chooses which one of three methods to call:
98
+
99
+
1. First: try `obj[Symbol.toPrimitive](hint)` if exists.
100
+
2. Otherwise:
101
+
1. For `hint == "string"` try to call `obj.toString()` and then `obj.valueOf()`.
102
+
2. for `hint == "number" or "default"` we try to call `obj.valueOf()` and then `obj.toString()`.
103
+
104
+
-->
105
+
106
+
107
+
### `toString` and `valueOf`
108
+
109
+
If there is no `Symbol.toPrimitive`, then for string conversions `toString` is tried and then `valueOf`, while for numeric or default conversions the order is `valueOf` -> `toString`.
110
+
111
+
If the result of either method is not an object, then it is ignored.
112
+
113
+
For instance, this `user` does the same as above:
71
114
72
-
Now let's add a custom numeric conversion with `valueOf`:
73
115
74
116
```js run
75
117
let user = {
76
-
77
-
name:'John',
118
+
name:"John",
78
119
age:30,
79
120
80
-
*!*
121
+
// for hint="string"
122
+
toString() {
123
+
returnthis.name;
124
+
},
125
+
126
+
// for hint="number" or "default"
81
127
valueOf() {
82
128
returnthis.age;
83
129
}
84
-
*/!*
85
130
86
131
};
87
132
88
-
*!*
89
-
alert(+user ); // 30
90
-
*/!*
133
+
alert(user); // John
134
+
alert(+user);// 30
135
+
alert(user +1); // 31 (default like number calls valueOf)
91
136
```
92
137
93
-
In most projects though, only `toString()` is used, because objects are printed out (especially for debugging) much more often than added/substracted/etc.
94
-
95
-
If only `toString()` is implemented, then both string and numeric conversions use it.
138
+
In most practical cases, only `toString` is implemented. Then it is used for both conversions.
96
139
97
-
## Array example
140
+
```smart header="Methods must return a primitive, but its type is not guaranteed"
141
+
If `toString/valueOf` return a non-primitive value, it is ignored. For `Symbol.toPrimitive`, it's even stricter: non-primitive is automatically an error. So the result of `ToPrimitive` algorithm as a whole can only be primitive.
98
142
99
-
Let's see few more examples with arrays to get the better picture.
143
+
But it can be any primitive. There's no control whether `toString()` returns exactly a string or, say, a boolean.
100
144
101
-
```js run
102
-
alert( [] +1 ); // '1'
103
-
alert( [1] +1 ); // '11'
104
-
alert( [1,2] +1 ); // '1,21'
145
+
If `ToPrimitive` is a part of a `ToNumber/ToString` transform, then after it there's one more step to transform the primitive to a string or a number.
105
146
```
106
147
107
-
The array from the left side of `+` is first converted to primitive using `toPrimitive(obj, "number")`.
108
-
109
-
For arrays (and most other built-in objects) only `toString` is implemented, and it returns a list of items.
148
+
````smart header="ToBoolean?"
110
149
111
-
So we'll have the following results of conversion:
150
+
There is no such thing as `ToBoolean`. All objects (even empty) are `true` in boolean context:
112
151
113
-
```js
114
-
alert( ''+1 ); // '1'
115
-
alert( '1'+1 ); // '11'
116
-
alert( '1,2'+1 ); // '1,21'
152
+
```js run
153
+
if ({}) alert("true"); // works
117
154
```
118
155
119
-
Now the addition has the first operand -- a string, so it converts the second one to a string also. Hence the result.
156
+
That is not customizable.
157
+
````
158
+
159
+
## Object: toString for the type
160
+
161
+
There are many kinds of built-in objects in Javascript. Many of them have own implementations of `toString` and `valueOf`. We'll see them when we get to them.
120
162
121
-
## Object, toStringfor the type
163
+
Here let's see the default `toString` and `valueOf`for plain objects.
122
164
123
-
With plain objects it's much more interesting.
165
+
### valueOf
124
166
125
-
An object has both `valueOf()` and `toString()`, but for plain objects`valueOf()` returns the object itself:
167
+
By default, there is a built-in `valueOf()`for objects, but it returns the object itself:
Because `ToPrimitive` ignores `valueOf` if it returns an object, here we can assume that `valueOf` does not exist at all.
175
+
Because `ToPrimitive`algorithm ignores `valueOf` if it returns an object, we can assume that `valueOf` does not exist at all.
134
176
135
-
Now `toString`.
177
+
### toString
178
+
179
+
The `toString()` is much, much more interesting.
136
180
137
181
From the first sight it's obvious:
138
182
@@ -150,45 +194,39 @@ The algorithm of the `toString()` for plain objects looks like this:
150
194
151
195
- If `this` value is `undefined`, return `[object Undefined]`
152
196
- If `this` value is `null`, return `[object Null]`
153
-
- ...For arrays return `[object Array]`, for dates return `[object Date]` etc.
154
-
197
+
- If `this` is a function, return `[object Function]`
198
+
- ...Then some other builtin cases...
199
+
- Otherwise return `[object @@toStringTag]`, where `@@toStringTag` is the value of `obj[Symbol.toStringTag]`.
200
+
- Or, if no `toStringTag`, then return `[object Object]`.
155
201
156
-
It even works for environment-specific objects that exist only in the browser (like `window`) or in node.js (like `process`).
202
+
Most environment-specific objects even if they do not belong to Javascript core, like `window` in the browser or `process`in Node.JS, carry `Symbol.toStringTag` property.
157
203
158
-
All we need to do to get the type of an `obj` -- is to call plain object `toString` passing `this = obj`.
204
+
So this algorithm appears to be really universal.
159
205
160
-
We can do it like this:
206
+
To make use of it, we should pass the thing to examine as `this`. We can do it using `func.call`:
161
207
162
208
```js run
163
-
let s = {}.toString; // copy toString of Object to a variable
164
-
165
-
// what type is this?
166
-
let arr = [];
209
+
let s = {}.toString; // copy toString of a plain object to a variable
//try getting the type of a browser window object?
174
-
window.toStringPlain= s;
175
-
176
-
alert( window.toStringPlain() ); //[object Window] <-- it works!
217
+
//browser object example works too
218
+
alert( s.call(window) ); // [object Window]
219
+
// (because it has Symbol.toStringTag)
220
+
alert( window[Symbol.toStringTag] ); // Window
177
221
```
178
222
179
-
Please note that different objects usually have their own `toString`. As we've seen above, the `toString` of `Array` returns a list of items. So we need to use exactly the `toString` of a plain object -- `{}.toString`.
180
-
181
-
To call it in the right context, we copy it into a variable `s` -- in Javascript functions are not hardwired to objects, even built-in ones, so we do it -- and then assign as a property to another object `arr.toStringPlain` (not to override `arr.toString`). That's called *method borrowing*.
182
-
183
-
Actually, we could evade all complexities using [call](info:object-methods#call-apply) to pass `this`:
223
+
In the example above we copy the "original" `toString` method of a plain object to the variable `s`, and then use it to make sure that we use *exactly that*`toString`.
0 commit comments