Skip to content

Commit a1d8ddf

Browse files
committed
types-grammar, ch4: starting chapter on coercion
1 parent a665a55 commit a1d8ddf

File tree

4 files changed

+184
-4
lines changed

4 files changed

+184
-4
lines changed

types-grammar/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@
1111
* [Chapter 1: Primitives](ch1.md)
1212
* [Chapter 2: Value Behaviors](ch2.md)
1313
* [Chapter 3: Object Values](ch3.md)
14-
* Chapter 4: TODO
14+
* [Chapter 4: Coercing Values](ch4.md)
15+
* Chapter 5: TODO

types-grammar/ch3.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The "Objects & Classes" title of this series covers objects in-depth already, so
1414
The `object` value-type comprises several sub-types with specialized behaviors. These include:
1515

1616
* general objects
17-
* primitive objects
17+
* fundamental objects (boxed primitives)
1818
* arrays
1919
* regular expressions
2020
* functions (aka, "callable objects")
@@ -23,7 +23,7 @@ But one shared characteristic is that all objects are capable of acting as colle
2323

2424
// TODO
2525

26-
## Primitives As Objects
26+
## Fundamental Objects
2727

2828
In Chapter 2, I briefly mentioned *auto-boxing*, which we'll now revisit.
2929

types-grammar/ch4.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# You Don't Know JS Yet: Types & Grammar - 2nd Edition
2+
# Chapter 4: Coercing Values
3+
4+
| NOTE: |
5+
| :--- |
6+
| Work in progress |
7+
8+
We've thouroughly covered all of the different *types* of values in JS. And along the way, more than a few times, we mentioned the notion of converting -- actually, coercing -- from one type of value to another.
9+
10+
In this chapter, we'll dive deep into coercion and uncover all its mysteries.
11+
12+
## Abstracts
13+
14+
The JS specification details a number of *abstract operations*[^AbstractOperations] that dictate internal conversion from one value-type to another. It's important to be aware of these operations, as coercive mechanics in the language mix and match them in various ways.
15+
16+
These operations *look* as if they're real functions that could be called, such as `ToString(..)` or `ToNumber(..)`. But by *abstract*, we mean they only exist conceptually by these names; they aren't functions we can *directly* invoke in our programs. Instead, we invoke them implicitly/indirectly depending on the statements/expressions in our programs.
17+
18+
### ToPrimitive
19+
20+
Any value that's not already a primitive can be reduced to a primitive using the `ToPrimitive()` (specifically, `OrdinaryToPrimitive()`[^OrdinaryToPrimitive]) abstract operation. Generally, the `ToPrimitive()` is given a *hint* to tell it whether a `number` or `string` is preferred.
21+
22+
```
23+
// ToPrimitive() is abstract
24+
25+
ToPrimitive({ a: 1 },"string"); // "[object Object]"
26+
27+
ToPrimitive({ a: 1 },"number"); // NaN
28+
```
29+
30+
The `ToPrimitive()` operation will look on the object provided, for either a `toString()` method or a `valueOf()` method; the order it looks for those is controlled by the *hint*.
31+
32+
If the method returns a value matching the *hinted* type, the operation is finished. But if the method doesn't return a value of the *hinted* type, `ToPrimitive()` will then look for and invoke the other method (if found).
33+
34+
If the attempts at method invocation fail to produce a value of the *hinted* type, the final return value is forcibly coerced via the corresponding abstract operation: `ToString()` or `ToNumber()`.
35+
36+
### ToString
37+
38+
Pretty much any value that's not already a string can be coerced to a string representation, via `ToString()`. [^ToString] This is usually quite intuitive, especially with primitive values:
39+
40+
```
41+
// ToString() is abstract
42+
43+
ToString(42.0); // "42"
44+
ToString(-3); // "-3"
45+
ToString(Infinity); // "Infinity"
46+
ToString(NaN); // "NaN"
47+
ToString(42n); // "42"
48+
49+
ToString(true); // "true"
50+
ToString(false); // "false"
51+
52+
ToString(null); // "null"
53+
ToString(undefined); // "undefined"
54+
```
55+
56+
There are *some* results that may vary from common intuition. As mentioned in Chapter 2, very large or very small numbers will be represented using scientific notation:
57+
58+
```
59+
ToString(Number.MAX_VALUE); // "1.7976931348623157e+308"
60+
ToString(Math.EPSILON); // "2.220446049250313e-16"
61+
```
62+
63+
Another counter-intuitive result comes from `-0`:
64+
65+
```
66+
ToString(-0); // "0" -- wtf?
67+
```
68+
69+
This isn't a bug, it's just an intentional behavior from the earliest days of JS, based on the assumption that developers generally wouldn't want to ever see a negative-zero output.
70+
71+
One primitive value-type that is *not allowed* to be coerced (implicitly, at least) to string is `symbol`:
72+
73+
```
74+
ToString(Symbol("ok")); // TypeError exception thrown
75+
```
76+
77+
| WARNING: |
78+
| :--- |
79+
| Calling the `String()`[^StringFunction] concrete function (without `new` operator) is generally thought of as *merely* invoking the `ToString()` abstract operation. While that's mostly true, it's not entirely so. `String(Symbol("ok"))` works, whereas the abstract `ToString(Symbol(..))` itself throws an exception. |
80+
81+
#### Default `toString()`
82+
83+
When `ToString()` is performed on object value-types, it instead invokes the `ToPrimitive()` operation (as explained earlier), with `"string"` as its *hinted* type:
84+
85+
```
86+
ToString(new String("abc")); // "abc"
87+
ToString(new Number(42)); // "42"
88+
89+
ToString({ a: 1 }); // "[object Object]"
90+
ToString([ 1, 2, 3 ]); // "1,2,3"
91+
```
92+
93+
By virtue of `ToPrimitive(..,"string")` delegation, these objects all have their default `toString()` method (inherited via `[[Prototype]]`) invoked.
94+
95+
### ToNumber
96+
97+
Non-number values *that resemble* numbers, such as numeric strings, can generally be coerced to a numeric representation, using `ToNumber()`: [^ToNumber]
98+
99+
```
100+
// ToNumber() is abstract
101+
102+
ToNumber("42"); // 42
103+
ToNumber("-3"); // -3
104+
ToNumber("1.2300"); // 1.23
105+
ToNumber(" 8.0 "); // 8
106+
```
107+
108+
If the full value doesn't *completely* (other than whitespace) resemble a valid number, the result will be `NaN`:
109+
110+
```
111+
ToNumber("123px"); // NaN
112+
ToNumber("hello"); // NaN
113+
```
114+
115+
Other primitive values have certain designated numeric equivalents:
116+
117+
```
118+
ToNumber(true); // 1
119+
ToNumber(false); // 0
120+
121+
ToNumber(null); // 0
122+
ToNumber(undefined); // NaN
123+
```
124+
125+
There are some rather surprising designations for `ToNumber()`:
126+
127+
```
128+
ToNumber(""); // 0
129+
ToNumber(" "); // 0
130+
```
131+
132+
| NOTE: |
133+
| :--- |
134+
| I call these "surprising" because I think it would have made much more sense for them to coerce to `NaN`, the way `undefined` does. |
135+
136+
Some primitive values are *not allowed* to be coerced to numbers, and result in exceptions rather than `NaN`:
137+
138+
```
139+
ToNumber(42n); // TypeError exception thrown
140+
ToNumber(Symbol("42")); // TypeError exception thrown
141+
```
142+
143+
| WARNING: |
144+
| :--- |
145+
| Calling the `Number()`[^NumberFunction] concrete function (without `new` operator) is generally thought of as *merely* invoking the `ToNumber()` abstract operation to coerce a value to a number. While that's mostly true, it's not entirely so. `Number(42n)` works, whereas the abstract `ToNumber(42n)` itself throws an exception. |
146+
147+
#### Default `valueOf()`
148+
149+
When `ToNumber()` is performed on object value-types, it instead invokes the `ToPrimitive()` operation (as explained earlier), with `"number"` as its *hinted* type:
150+
151+
```
152+
ToNumber(new String("abc")); // NaN
153+
ToNumber(new Number(42)); // 42
154+
155+
ToNumber({ a: 1 }); // NaN
156+
ToNumber([ 1, 2, 3 ]); // NaN
157+
ToNumber([]); // 0
158+
```
159+
160+
By virtue of `ToPrimitive(..,"number")` delegation, these objects all have their default `valueOf()` method (inherited via `[[Prototype]]`) invoked.
161+
162+
[^AbstractOperations]: "7.1 Type Conversion", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-type-conversion ; Accessed August 2022
163+
164+
[^OrdinaryToPrimitive]: "7.1.1.1 OrdinaryToPrimitive(O,hint)", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-ordinarytoprimitive ; Accessed August 2022
165+
166+
[^ToString]: "7.1.17 ToString(argument)", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-tostring ; Accessed August 2022
167+
168+
[^StringConstructor]: "22.1.1 The String Constructor", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-string-constructor ; Accessed August 2022
169+
170+
[^StringFunction]: "22.1.1.1 String(value)", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-string-constructor-string-value ; Accessed August 2022
171+
172+
[^ToNumber]: "7.1.4 ToNumber(argument)", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-tonumber ; Accessed August 2022
173+
174+
[^NumberConstructor]: "21.1.1 The Number Constructor", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-number-constructor ; Accessed August 2022
175+
176+
[^NumberFunction]: "21.1.1.1 Number(value)", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-number-constructor-number-value ; Accessed August 2022

types-grammar/toc.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,8 @@
2525
* Primitives Are Foundational
2626
* Chapter 3: Object Values
2727
* Nature Of Objects
28-
* Primitives As Objects
28+
* Fundamental Objects
29+
* TODO
30+
* Chapter 4: Coercing Values
31+
* Abstracts
2932
* TODO

0 commit comments

Comments
 (0)