Skip to content

Commit 1bffa43

Browse files
committed
ok
1 parent 514340e commit 1bffa43

File tree

1 file changed

+131
-93
lines changed
  • 1-js/2-first-steps/20-object-tostring-valueof

1 file changed

+131
-93
lines changed

1-js/2-first-steps/20-object-tostring-valueof/article.md

Lines changed: 131 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -5,134 +5,178 @@ In the chapter <info:type-conversions> we've seen the rules for numeric, string
55

66
But we left a gap for objects. Now let's fill it.
77

8+
89
[cut]
910

10-
For objects, there's a special additional conversion called [ToPrimitive](https://tc39.github.io/ecma262/#sec-toprimitive).
11+
## Where and why?
1112

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.
1314

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.
1616

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.
1818

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.
2020

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.
2222

23-
`ToPrimitive` is customizable via methods `toString()` and `valueOf()`.
23+
That said, there are still valid reasons why we should know how it works.
2424

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.
2628

2729

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
3331

32+
When an object is used as a primitive, the special internal algorithm named [ToPrimitive](https://tc39.github.io/ecma262/#sec-toprimitive) is invoked.
3433

35-
The `ToPrimitive(obj, "number")` is the same, but `valueOf()` and `toString()` are swapped:
34+
It comes in 3 flavours:
3635

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
4239

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:
4541

46-
But it can be any primitive. There's no control whether `toString()` returns exactly a string or, say a boolean.
47-
```
4842

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:
5072

5173
```js run
5274
let user = {
75+
name: "John",
76+
age: 30,
5377

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;
5982
}
60-
*/!*
61-
83+
6284
};
6385

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
6790
```
6891

69-
Looks much better than the default `[object Object]`, right?
7092

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:
71114

72-
Now let's add a custom numeric conversion with `valueOf`:
73115

74116
```js run
75117
let user = {
76-
77-
name: 'John',
118+
name: "John",
78119
age: 30,
79120

80-
*!*
121+
// for hint="string"
122+
toString() {
123+
return this.name;
124+
},
125+
126+
// for hint="number" or "default"
81127
valueOf() {
82128
return this.age;
83129
}
84-
*/!*
85130

86131
};
87132

88-
*!*
89-
alert( +user ); // 30
90-
*/!*
133+
alert(user); // John
134+
alert(+user); // 30
135+
alert(user + 1); // 31 (default like number calls valueOf)
91136
```
92137

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.
96139

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.
98142
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.
100144
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.
105146
```
106147

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?"
110149
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:
112151
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
117154
```
118155
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.
120162

121-
## Object, toString for the type
163+
Here let's see the default `toString` and `valueOf` for plain objects.
122164

123-
With plain objects it's much more interesting.
165+
### valueOf
124166

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:
126168

127169
```js run
128170
let obj = { };
129171

130172
alert( obj === obj.valueOf() ); // true, valueOf returns the object itself
131173
```
132174

133-
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.
134176

135-
Now `toString`.
177+
### toString
178+
179+
The `toString()` is much, much more interesting.
136180

137181
From the first sight it's obvious:
138182

@@ -150,45 +194,39 @@ The algorithm of the `toString()` for plain objects looks like this:
150194

151195
- If `this` value is `undefined`, return `[object Undefined]`
152196
- 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]`.
155201

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.
157203

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.
159205

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`:
161207

162208
```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
167210

168-
// copy Object toString to it:
169-
arr.toStringPlain = s;
211+
// null example
212+
alert( s.call(null) ); // [object Null]
170213

171-
alert( arr.toStringPlain() ); // [object Array] <-- right!
214+
// function example
215+
alert( s.call(alert) ); // [object Function]
172216

173-
// 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
177221
```
178222

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`.
184224

225+
We could also call it directly:
185226
```js run
186-
let arr = [];
187-
188-
alert( {}.toString.call(arr) ); // [object Array]
189-
alert( {}.toString.call(window) ); // [object Window]
227+
alert( {}.toString.call("test") ); // [object String]
190228
```
191229

192-
Here we do the same in one line: get the `toString` of a plain object and call it with the right `this` to get its type.
230+
193231

194232

0 commit comments

Comments
 (0)