From c714756f8f488fd5cb08027b40f0d9709eb14ec1 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 15 Apr 2022 23:48:06 +0300 Subject: [PATCH] minor fixes --- .../09-object-toprimitive/article.md | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/1-js/04-object-basics/09-object-toprimitive/article.md b/1-js/04-object-basics/09-object-toprimitive/article.md index 31bcc279..872c1f5b 100644 --- a/1-js/04-object-basics/09-object-toprimitive/article.md +++ b/1-js/04-object-basics/09-object-toprimitive/article.md @@ -7,11 +7,11 @@ JavaScript doesn't exactly allow to customize how operators work on objects. Unl In case of such operations, objects are auto-converted to primitives, and then the operation is carried out over these primitives and results in a primitive value. -That's an important limitation, as the result of `obj1 + obj2` can't be another object! +That's an important limitation: the result of `obj1 + obj2` (or another math operation) can't be another object! E.g. we can't make objects representing vectors or matrices (or achievements or whatever), add them and expect a "summed" object as the result. Such architectural feats are automatically "off the board". -So, because we can't do much here, there's no maths with objects in real projects. When it happens, it's usually because of a coding mistake. +So, because we can't technically do much here, there's no maths with objects in real projects. When it happens, with rare exceptions, it's because of a coding mistake. In this chapter we'll cover how an object converts to primitive and how to customize it. @@ -24,15 +24,19 @@ We have two purposes: In the chapter 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 fill it. -1. All objects are `true` in a boolean context. There are only numeric and string conversions. +1. There's no conversion to boolean. All objects are `true` in a boolean context, as simple as that. There exist only numeric and string conversions. 2. The numeric conversion happens when we subtract objects or apply mathematical functions. For instance, `Date` objects (to be covered in the chapter ) can be subtracted, and the result of `date1 - date2` is the time difference between two dates. -3. As for the string conversion -- it usually happens when we output an object like `alert(obj)` and in similar contexts. +3. As for the string conversion -- it usually happens when we output an object with `alert(obj)` and in similar contexts. -We can fine-tune string and numeric conversion, using special object methods. +We can implement string and numeric conversion by ourselves, using special object methods. -There are three variants of type conversion, that happen in various situations. +Now let's get into technical details, because it's the only way to cover the topic in-depth. -They're called "hints", as described in the [specification](https://tc39.github.io/ecma262/#sec-toprimitive): +## Hints + +How does JavaScript decide which conversion to apply? + +There are three variants of type conversion, that happen in various situations. They're called "hints", as described in the [specification](https://tc39.github.io/ecma262/#sec-toprimitive): `"string"` : For an object-to-string conversion, when we're doing an operation on an object that expects a string, like `alert`: @@ -60,10 +64,12 @@ They're called "hints", as described in the [specification](https://tc39.github. let greater = user1 > user2; ``` + Most built-in mathematical functions also include such conversion. + `"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. So if a binary plus gets an object as an argument, it uses the `"default"` hint to convert it. + For instance, binary plus `+` can work both with strings (concatenates them) and numbers (adds them). So if a binary plus gets an object as an argument, it uses the `"default"` hint to convert it. Also, if an object is compared using `==` with a string, number or a symbol, it's also unclear which conversion should be done, so the `"default"` hint is used. @@ -77,21 +83,19 @@ They're called "hints", as described in the [specification](https://tc39.github. The greater and less comparison operators, such as `<` `>`, can work with both strings and numbers too. Still, they use the `"number"` hint, not `"default"`. That's for historical reasons. - In practice though, we don't need to remember these peculiar details, because all built-in objects except for one case (`Date` object, we'll learn it later) implement `"default"` conversion the same way as `"number"`. And we can do the same. +In practice though, things are a bit simpler. -```smart header="No `\"boolean\"` hint" -Please note -- there are only three hints. It's that simple. +All built-in objects except for one case (`Date` object, we'll learn it later) implement `"default"` conversion the same way as `"number"`. And we probably should do the same. -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. -``` +Still, it's important to know about all 3 hints, soon we'll see why. **To do the conversion, JavaScript tries to find and call three object methods:** 1. Call `obj[Symbol.toPrimitive](hint)` - the method with the symbolic key `Symbol.toPrimitive` (system symbol), if such method exists, 2. Otherwise if hint is `"string"` - - try `obj.toString()` and `obj.valueOf()`, whatever exists. + - try calling `obj.toString()` or `obj.valueOf()`, whatever exists. 3. Otherwise if hint is `"number"` or `"default"` - - try `obj.valueOf()` and `obj.toString()`, whatever exists. + - try calling `obj.valueOf()` or `obj.toString()`, whatever exists. ## Symbol.toPrimitive @@ -128,12 +132,11 @@ 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 If there's no `Symbol.toPrimitive` then JavaScript tries to find methods `toString` and `valueOf`: -- For the "string" hint: `toString`, and if it doesn't exist, then `valueOf` (so `toString` has the priority for string conversions). +- For the `"string"` hint: call `toString` method, and if it doesn't exist, then `valueOf` (so `toString` has the priority for string conversions). - For other hints: `valueOf`, and if it doesn't exist, then `toString` (so `valueOf` has the priority for maths). 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. @@ -207,23 +210,23 @@ In the absence of `Symbol.toPrimitive` and `valueOf`, `toString` will handle all 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"`. +There is no control whether `toString` returns exactly a string, or whether `Symbol.toPrimitive` method returns a number for the hint `"number"`. The only mandatory thing: these methods must return a primitive, not an object. ```smart header="Historical notes" For historical reasons, if `toString` or `valueOf` returns an object, there's no error, but such value is ignored (like if the method didn't exist). That's because in ancient times there was no good "error" concept in JavaScript. -In contrast, `Symbol.toPrimitive` *must* return a primitive, otherwise there will be an error. +In contrast, `Symbol.toPrimitive` is stricter, it *must* return a primitive, otherwise there will be an error. ``` ## Further conversions As we know already, many operators and functions perform type conversions, e.g. multiplication `*` converts operands to numbers. -If we pass an object as an argument, then there are two stages: +If we pass an object as an argument, then there are two stages of calculations: 1. The object is converted to a primitive (using the rules described above). -2. If the resulting primitive isn't of the right type, it's converted. +2. If the necessary for further calculations, the resulting primitive is also converted. For instance: @@ -260,18 +263,18 @@ The object-to-primitive conversion is called automatically by many built-in func There are 3 types (hints) of it: - `"string"` (for `alert` and other operations that need a string) - `"number"` (for maths) -- `"default"` (few operators) +- `"default"` (few operators, usually objects implement it the same way as `"number"`) -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 specification describes explicitly which operator uses which hint. 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. + - try calling `obj.toString()` or `obj.valueOf()`, whatever exists. 3. Otherwise if hint is `"number"` or `"default"` - - try `obj.valueOf()` and `obj.toString()`, whatever exists. + - try calling `obj.valueOf()` or `obj.toString()`, whatever exists. -In practice, it's often enough to implement only `obj.toString()` as a "catch-all" method for string conversions that should return a "human-readable" representation of an object, for logging or debugging purposes. +All these methods must return a primitive to work (if defined). -As for math operations, JavaScript doesn't provide a way to "override" them using methods, so real life projects rarely use them on objects. +In practice, it's often enough to implement only `obj.toString()` as a "catch-all" method for string conversions that should return a "human-readable" representation of an object, for logging or debugging purposes.