This commit is contained in:
Ilya Kantor 2016-07-30 17:29:58 +03:00
parent eb49340d65
commit 9064e35f3f
9 changed files with 188 additions and 198 deletions

View file

@ -1,17 +1,13 @@
# Objects
As we know, there are 7 language types in Javascript.
As we know, there are 7 language types in Javascript. Six of them are called "primitive", because their values contain only a single thing (be it a string or a number or whatever).
Six of them are called "primitive", because their values contain only a single thing (be it a string or a number or whatever).
In contrast, objects are used to store *keyed collections* of various data and more complex entities.
In Javascript, objects penetrate almost every aspect of the language. So we must understand them first before going in-depth anywhere else.
In contrast, objects are used to store keyed collections of various data and more complex entities. In Javascript, objects penetrate almost every aspect of the language. So we must understand them first before going in-depth anywhere else.
[cut]
An object is defined with the figure brackets `{…}` with an optional list of "key: value" pairs. In programming that's sometimes called an "associative array" or a "hash".
An object can be created with figure brackets `{…}` with an optional list of "key: value" pairs. In programming that's sometimes called an "associative array" or a "hash".
We can imagine an object as a cabinet with signed files. Every piece of data is stored in it's file by the key. It's easy to find a file by it's name or add/remove a file.
@ -144,14 +140,14 @@ let bag = {
alert( bag.apple ); // 5 if fruit="apple"
```
Here, the object `bag` is created with a property with the name from `fruit` variable and the value `5`. So, if a visitor enters `"apple"`, `bag` will become `{apple: 5}`.
Here, the value of `fruit` variable is used as the property name. So, if a visitor enters `"apple"`, `bag` will become `{apple: 5}`.
Essentially, that works the same as:
```js run
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};
// take property name from the fruit variable, assign 5 to it
// take property name from the fruit variable
bag[fruit] = 5;
```
@ -160,7 +156,7 @@ We can use more complex expressions inside square brackets. Anything that result
```js
let fruit = 'apple';
let bag = {
[ 'apple' + 'Computers' ]: 5 // bag.appleComputers = 5
['apple' + 'Computers']: 5 // bag.appleComputers = 5
};
```

View file

@ -12,7 +12,9 @@ The main concept of memory management in Javascript is *reachability*.
Simply put, "reachable" values are those that are accessible now or in the future. They are guaranteed to be stored in memory.
1. There's a base set of inherently reachable values. For instance:
1. There's a base set of inherently reachable values, that cannot be deleted for obvious reasons.
For instance:
- Local variables and parameters of the current function.
- Variables and parameters for other functions on the current chain of nested calls.
@ -38,9 +40,9 @@ let user = {
![](memory-user-john.png)
Here an on further pictures, arrows depict references. For instance, the global variable `"user"` references John (the object `{name: "John"}`). The `"name"` property is not a reference, it stores a primitive, so it's painted inside the object.
Here the arrow depicts an object reference. The global variable `"user"` references the object `{name: "John"}` (we'll call it John for brevity). The `"name"` property of John stores a primitive, so it's painted inside the object.
If the `user` is overwritten, the reference is lost:
If the value of `user` is overwritten, the reference is lost:
```js
user = null;
@ -102,22 +104,24 @@ The resulting memory structure:
![](family.png)
To activate garbage collection, let's remove two references:
As of now, all objects are reachable.
Now let's remove two references:
```js
delete family.father;
delete family.mother.husband;
```
Note that if the deletion of any single one of them would not lead to anything, because all objects would still be reachable.
![](family-delete-refs.png)
It's not enough to delete any one of them, because all objects would still be reachable.
But if we delete both, then we can see that John has no incoming references any more:
![](family-no-father.png)
Outgoing references do not matter. Only incoming ones can make an object reachable.
John, the former `family.father` is now unreachable and will be removed from the memory with all its data that also became unaccessible.
Outgoing references do not matter. Only incoming ones can make an object reachable. So, John is now unreachable and will be removed from the memory with all its data that also became unaccessible.
After garbage collection:
@ -133,15 +137,15 @@ The source object is the same as above. Then:
family = null;
```
The result:
The in-memory picture becomes:
![](family-no-family.png)
This example demonstrates how important the concept of reachability is.
It is clearly seen that John and Ann are still linked, both have incoming references. But it's not enough.
It's obvious that John and Ann are still linked, both have incoming references. But that's not enough.
The former `"family"` object has been unlinked from the root, there's no reference to it any more, so the whole island became unreachable and will be removed.
The former `"family"` object has been unlinked from the root, there's no reference to it any more, so the whole island becomes unreachable and will be removed.
## Internal algorithms
@ -151,10 +155,12 @@ Regularly the following "garbage collection" steps are performed:
- The garbage collector takes roots and "marks" them.
- Then it visits and "marks" all references from them.
- Then it visits marked objects and marks *their* references, but the same object is not visited twice.
- Then it visits marked objects and marks *their* references. All visited objects are remembered, not to visit the same object twice in the future.
- ...And so on until there are unvisited references (reachable from the roots).
- All objects except marked ones are removed.
For instance, if our object structure looks like this:
![](garbage-collection-1.png)
@ -182,10 +188,10 @@ Javascript engines apply many optimizations to it, to make it run faster and be
Some of the optimizations:
- **Generational collection** -- objects are split into two sets: "new ones" and "old ones". Many objects appear, do their job and die fast, so they can be cleaned up more aggressively. Those "new" that survive for long enough, become "old".
- **Incremental collection** -- if there are many objects, and we try to walk and mark the whole object set at once, it may take some time and introduce visible delays. So the engine tries to split the job into pieces. Then pieces are executed one at a time. That requires some extra bookkeeping in-between them to stay consistent.
- **Incremental collection** -- if there are many objects, and we try to walk and mark the whole object set at once, it may take some time and introduce visible delays. So the engine tries to split the job into pieces. Then pieces are executed one at a time. That requires some extra bookkeeping between them.
- **Idle-time collection** -- the garbage collector tries to run only while the CPU is idle, to reduce the possible effect on the execution.
In-depth understanding of these optimization is also possible, there's no magic, but it requires a lot of under-the-hood digging. Javascript engines implement garbage collection differently. And -- what's even more important, things change, so going really deep "in advance" is probably not worth that. Unless, of course, it is a matter of pure interest.
Detailed learning of these optimization is also possible, but it requires a lot of under-the-hood digging. Javascript engines implement garbage collection differently. And -- what's even more important, things change, so going really deep "in advance", without a real need is probably not worth that. Unless, of course, it is a matter of pure interest, then there will be some links for you below.
## Summary
@ -197,7 +203,7 @@ The main things to know:
Modern engines implement advanced algorithms of garbage collection.
If you are familiar with low-level programming, the more detailed information about V8 garbage collector is in the article [A tour of V8: Garbage Collection](http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection). Also you'd better prepare yourself by learning how values are stored in V8. I'm saying: "V8", because it is best covered with articles in the internet. For other engines, things are somewhat similar, but not the same.
If you are familiar with low-level programming, the more detailed information about V8 garbage collector is in the article [A tour of V8: Garbage Collection](http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection), and [V8 blog](http://v8project.blogspot.com/) also publishes articles about changes in memory management from time to time. Also you'd better prepare yourself by learning about V8 in general and read the blog of [Vyacheslav Egorov](http://mrale.ph) who worked as one of V8 engineers. I'm saying: "V8", because it is best covered with articles in the internet. For other engines, many approaches are similar, but garbage collection differs in many aspects.
In-depth knowledge of engines is good when you need low-level optimizations. It would be wise to plan that as the next step after you're familiar with the language.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -42,12 +42,6 @@ A function that is the property of an object is called its *method*.
So, here we've got a method `sayHi` of the object `user`.
```smart header="Object-oriented programming"
When we write our code using objects to represent entities, that's called an [object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming), in short: "OOP".
OOP is a big thing, an interesting science of its own. How to choose the right entities? How to organize the interaction between them? That's architecture. We will make use of OOP further when we get enough familarity with the language.
```
Of course, we could use a Function Declaration to add a method:
```js run
@ -70,6 +64,11 @@ user.sayHi(); // Hello!
That would also work, but is longer. Also we get an "extra" function `sayHi` outside of the `user` object. Usually we don't want that.
```smart header="Object-oriented programming"
When we write our code using objects to represent entities, that's called an [object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming), in short: "OOP".
OOP is a big thing, an interesting science of its own. How to choose the right entities? How to organize the interaction between them? That's architecture.
```
### Method shorthand
There exists a shorter syntax for methods in an object literal:
@ -93,7 +92,7 @@ let user = {
};
```
As demonstrated, we can omit a colon with the word `"function"`.
As demonstrated, we can omit `"function"` and just write `sayHi()`.
To say the truth, the notations are not fully identical. There are subtle differences related to object inheritance (to be covered later), but for now they do not matter. In almost all cases the shorter syntax is preferred.
@ -218,6 +217,14 @@ In non-strict mode (if you forgot `use strict`) the value of `this` in such case
Please note that usually a call of a function using `this` without an object is not normal, but rather a programming mistake. If a function has `this`, then it is usually meant to be called in the context of an object.
```smart header="The consequences of unbound `this`"
If you come from another programming languages, then you are probably used to an idea of a "bound `this`", where methods defined in an object always have `this` referencing that object.
The idea of unbound, run-time evaluated `this` has both pluses and minuses. From one side, a function can be reused for different objects. From the other side, it's possible to occasionally loose `this` by making an improper call.
Here we are not to judge whether this language design decision is good or bad. We will understand how to work with it, how to get benefits and evade problems.
```
## Internals: Reference Type
An intricate method call can loose `this`, for instance:
@ -290,75 +297,6 @@ Any other operation like assignment `hi = user.hi` discards the reference type a
So, as the result, the value of `this` is only passed the right way if the function is called directly using a dot `obj.method()` or square brackets `obj[method]()` syntax (they do the same here).
## Methods on primitives [todo: remove]
In JavaScript primitives (strings, numbers etc) also have methods, as if they were objects. Still, primitives are not objects, and this section explains the interesting design solution of Javascript that makes it possible.
Let's formulate the definitive distinction between primitives and objects.
A primitive
: Is a value of one of 6 primitive types: `string`, `number`, `boolean`, `symbol`, `null` and `undefined`.
An object
: Can be created with `{}`, for instance: `{name: "John", age: 30}`, is capable of storing multiple values as properties. There are other kinds of objects in JavaScript, e.g. functions are objects, there are objects that work with dates, errors and so on. They have different properties and methods.
But features come at a price!
Objects are "heavier" than primitives. They require additional resources to support the internal machinery. But properties and methods are useful in programming, Javascript engines try to optimize them, so the price is usually fair.
So here's the paradox faced by the creator of JavaScript:
- There are many things one would want to do with a primitive like a string or a number. Could be great to access them as methods.
- Primitives must be as fast and lightweight as possible.
The solution looks a little bit awkward, but here it is.
1. Primitives are still primitive. A single lightweight value, as described.
2. The language allows to access methods and properties of strings, numbers, booleans and symbols.
3. When it happens, a special "object wrapper" is created, it provides the functionality, and then is destroyed.
The "object wrappers" are different for each primitive type and are named specifically: `String`, `Number`, `Boolean` and `Symbol`. Thus they provide different sets of methods.
For instance, there exists a method [str.toUpperCase()](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) that returns the capitalized string.
Here's how it works:
```js run
let str = "Hello";
alert( str.toUpperCase() ); // HELLO
```
Simple, right? And here's what actually happens in `str.toUpperCase()`:
1. The string `str` is a primitive. So in the moment of accessing its property a special object is created that both knows the value of the string and has useful methods, like `toUpperCase()`.
2. That method runs and returns a new string (shown by `alert`).
3. The special object is destroyed, leaving the primitive `str` alone.
So, primitives can provide methods, but they still remain lightweight.
Of course, a JavaScript engine highly optimizes that process. Internally it may skip creation of the extra object at all. But it must adhere to the specification and behave as if it creates one.
A number has methods of it's own, for instance, [toFixed(n)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) rounds the number to the given precision:
```js run
let n = 1.23456;
alert( n.toFixed(2) ); // 1.23
```
We'll learn more specific methods in next chapters.
````warn header="null/undefined have no methods"
Special primitives `null` and `undefined` are exceptions. They have no corresponding "wrapper objects" and provide no methods. In a sense, they are "the most primitive".
An attempt to access a property of such value would give an error:
```js run
alert(null.test); // error
````
## Summary
[todo]

View file

@ -87,16 +87,16 @@ The constructor can't be called again, because it is not saved anywhere, just cr
## Return from constructors
Usually, constructors do not have a `return` statement. Their task is to write all necessary stuff into `this`, that automatically becomes the result.
Usually, constructors do not have a `return` statement. Their task is to write all necessary stuff into `this`, and it automatically becomes the result.
But if there is a `return` statement, then the rule is simple:
- If `return` is called with object, then it is returned instead of `this`.
- If `return` is called with a primitive, it's ignored.
In other words, `return` with an object returns that object, with a non-object value -- returns `this` as usual.
In other words, `return` with an object returns that object, otherwise `this` is returned.
For instance, returning an object:
For instance, here `return` overrides `this` by returning an object:
```js run
function BigUser() {
@ -109,23 +109,26 @@ function BigUser() {
alert( new BigUser().name ); // Godzilla, got that object
```
And here's an example with returning a string:
And here's an example with an empty `return` (or we could place a primitive after it, doesn't matter):
```js run
function SmallUser() {
this.name = "John";
return "Mouse"; // <-- returns a primitive
return; // finishes the execution, returns this
// ...
}
alert( new SmallUser().name ); // John, got "this" (mouse disappeared)
alert( new SmallUser().name ); // John
```
Once again, most of time constructors return nothing. Here we mention this special aspect of constructors for completeness.
Most of time constructors return nothing. Here we mention the special behavior with returning objects mainly for the sake of completeness.
````smart header="Omitting brackets"
By the way, we can omit brackets after `new` with zero arguments:
By the way, we can omit brackets after `new`, if it has no arguments:
```js
let user = new User; // <-- no brackets
@ -133,14 +136,14 @@ let user = new User; // <-- no brackets
let user = new User();
```
Omitting brackets here is not considered a "good style", but the syntax is allowed by specification.
Omitting brackets here is not considered a "good style", but the syntax is permitted by specification.
````
## Methods in constructor
Using constuctor functions to create objects gives a great deal of flexibility. The constructor may have parameters that define how to construct the object, what to put in it.
Using constuctor functions to create objects gives a great deal of flexibility. The constructor function may have parameters that define how to construct the object, what to put in it.
Let's add a method.
Of course, we can add to `this` not only properties, but methods as well.
For instance, `new User(name)` below creates an object with the given `name` and the method `sayHi`:
@ -159,24 +162,22 @@ let john = new User("John");
john.sayHi(); // My name is: John
*/!*
/*
/*
john = {
name: "John",
sayHi: function
sayHi: function() { ... }
}
*/
```
## Summary
In this chapter we studied the basics of object constructors.
- Constructor functions are regular functions, but there's a common agreement to name them with capital letter first.
- Constructor functions or, shortly, constructors, are regular functions, but there's a common agreement to name them with capital letter first.
- Constructor functions should only be called using `new`. Such call implies a creation of empty `this` at the start and returning the populated one at the end.
We can already use constructor functions to make multiple similar objects. But the topic is much wider than described here. So we'll return it later and cover more in-depth.
We can use constructor functions to make multiple similar objects. But the topic is much deeper than described here. So we'll return it later and cover more in-depth.
Now it's important to understand what `new` is, because Javascript provides constructor functions for many built-in language objects: like `Date` for dates, `Set` for sets and others that we plan to study.
As of now, it's important to understand what `new` is, because Javascript provides constructor functions for many built-in language objects: like `Date` for dates, `Set` for sets and others that we plan to study.

View file

@ -227,4 +227,6 @@ They are listed in the specification in the [Well-known symbols](https://tc39.gi
Symbols don't appear in `for..in` loops. As we'll see further, there are other means to get object properties which also ignore symbols, so they remain hidden.
Technically though, there is still a way to discover all symbols of an object with a build-in method [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols). Also there is a method named [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) that returns all keys of an object. So they are not completely hidden and private. But most libraries, built-in methods and syntax constructs adhere to a common agreement that they are. And the one who explicitly calls the aforementioned methods is not doing that occasionally, but knows exactly what he's doing.
Technically though, there is still a way to discover all symbols of an object with a build-in method [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols). Also there is a method named [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) that returns all keys of an object, including symbolic ones. So they are not completely hidden and private.
But most libraries, built-in methods and syntax constructs adhere to a common agreement that they are. And the one who explicitly calls the aforementioned methods probably understands well what he's doing.

View file

@ -10,135 +10,137 @@ But we left a gap for objects. Now let's fill it.
## Where and why?
The process of object to primitive conversion can be customized, here we'll see how to implement our own methods for it.
The process of object to primitive conversion can be customized, here we'll see how to implement our own methods for it. But first, let's see when it happens.
But first, let's note that conversion of an object to primitive value (a number or a string) is a rare thing in practice.
The conversion of an object to primitive value (a number or a string) is a rare thing in practice.
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.
Just think about cases when such conversion may be necessary. For instance, numeric conversion happens when we compare an object against a primitive: `user == 18`. But what such comparison actually means? Are we going to compare `18` against user's age? Then it would be more obvious to write `user.age == 18`? And it's easier to read it too.
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.
Or, for a string conversion... Where does it happen? Usually, when we output an object. But simple ways of object-as-string output like `alert(user)` are only used for debugging and logging purposes. In projects, the output is more complicated, we may need to tune it by additional parameters, so it should be implemented by special methods.
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.
So, 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.
That said, there are still valid reasons why we should know how it works.
That said, there are still valid reasons why we should know how to-primitive conversion works.
- The `alert(user)` kind of output is still used for logging and debugging.
- The object-as-string kind of output is still useful sometimes. Without a customized conversion it will show `[object Object]`.
- Many built-in objects implement their own to-primitive conversion, we plan to cover that.
- Sometimes it just happens (on mistake?), and we should understand what's going on.
- Okay, the final one. There are quizzes and questions on interviews that rely on that knowledge. Looks like people think it's a good sigh that person understands Javascript if he knows type conversions well.
## ToPrimitive
When an object is used as a primitive, the special internal algorithm named [ToPrimitive](https://tc39.github.io/ecma262/#sec-toprimitive) is invoked.
The algorithm of object-to-primitive conversion is called `ToPrimitive` in [the specification](https://tc39.github.io/ecma262/#sec-toprimitive).
It comes in 3 flavours:
There are 3 types (also called "hints") of object-to-primitive conversion:
- `ToPrimitive(obj, "string")`
- `ToPrimitive(obj, "number")`
- `ToPrimitive(obj)`, the so-called "default" flavour
`"string"`
: For object-to-string conversions, like:
When [ToString](https://tc39.github.io/ecma262/#sec-tostring) conversion is applied to objects, it does two steps:
```js
// output
alert(obj);
// using object as a property key
anotherObj[obj] = value;
```
`"number"`
: For object-to-number conversions, like:
1. `let primitive = ToPrimitive(obj, "string")`
2. `return ToString(primitive)`
```js
// explicit conversion
let num = Number(obj);
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.
// maths (except binary plus)
let n = +obj; // unary plus
let delta = date1 - date2;
When [ToNumber](https://tc39.github.io/ecma262/#sec-tonumber) conversion is applied to objects, it also does two similar steps:
// less/greater comparison
let greater = user1 > user2;
```
1. `let primitive = ToPrimitive(obj, "number")`
2. `return ToNumber(primitive)`
`"default"`
: Occurs in rare cases where it's not clear what is desired.
Again, `ToPrimitive` is applied first, then the primitive result is converted to number.
For instance:
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.
```js
// binary plus can work both with strings (concatenates) and numbers (adds)
let total = car1 + car2;
So, to understand `ToNumber` and `ToString` for objects, we should redirect ourselves to `ToPrimitive`.
// obj == string, number or symbol also uses default
if (user == 1) { ... };
```
## The modern style: Symbol.toPrimitive
Still, there's some inconsistency here. The greater/less operator `<>` can work with both strings and numbers, it compares them differently: dictionary order is used for strings. Still, it uses "number" hint. That's for historical reasons.
The internal `ToPrimitive(obj, hint)` call has two parameters:
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.
`obj`
: The object to transform.
Please note -- there's only three. That simple. There is no "boolean" (all objects are `true` in boolean context) or anything else. And if we treat `"default"` and `"number"` the same, like most builtins do, then there are only two conversions. Not many.
`hint`
: The flavour: one of `"string"`, `"number"` or `"default"`.
To do the conversion, Javascript tries to find and call these three object methods:
If the object has `Symbol.toPrimitive` method implemented, which it is called with the `hint`.
1. `Symbol.toPrimitive(hint)` if exists,
2. Otherwise if hint is `"string"`, try `toString()` and `valueOf()`, whatever exists.
3. Otherwise if hint is `"number"` or `"default"`, try `valueOf()` and `toString()`, whatever exists.
For instance:
### Symbol.toPrimitive
For instance, here `user` object implements the 1st method:
```js run
let user = {
name: "John",
age: 30,
money: 1000,
// must return a primitive
[Symbol.toPrimitive](hint) {
alert(`hint: ${hint}`);
return hint == "string" ? this.name : this.age;
return hint == "string" ? `{name: "${this.name}"}` : this.money;
}
};
// conversions demo:
alert(user); // hint: string -> John
alert(+user); // hint: number -> 30
alert(user + 1); // hint: default -> 31
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
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.
```smart header="`Symbol.toPrimitive` must return a primitive, but its type is not guaranteed"
The method `Symbol.toPrimitive` must return a primitive value, otherwise it will be an error.
### toString/valueOf
But it can be any primitive. There's no control whether the call returns exactly a string or, say, a boolean.
Methods `toString` and `valueOf` come from the ancient times. That's why they are not symbols. They provide an alternative "old-style" way to implement the conversion.
If `ToPrimitive` is a part of a `ToNumber/ToString` conversion, then after it there will be one more step to transform the primitive to a string or a number.
```
If there's no `Symbol.toPrimitive` then Javascript tries to find them and try in the order:
## The old style: toString and valueOf
- `toString -> valueOf` for "string" hint.
- `valueOf -> toString` otherwise.
If there is no `Symbol.toPrimitive`, then the other two methods are used:
- `toString` -- for string conversion,
- `valueOf` -- for numeric conversion.
These methods are not symbols, because they come from ancient times when no symbols existed.
The algorithm is:
1. If `hint == "string"` try to call `obj.toString()` and then `obj.valueOf()`.
2. If `hint == "number" or "default"` we try to call `obj.valueOf()` and then `obj.toString()`.
If the result of either method is not an object, then it is ignored.
For instance, this `user` does the same as above:
For instance, here `user` does the same as above using a combination of `toString` and `valueOf`:
```js run
let user = {
name: "John",
age: 30,
money: 1000,
// for hint="string"
toString() {
return this.name;
return `{name: "${this.name}"}`;
},
// for hint="number" or "default"
valueOf() {
return this.age;
return this.money;
}
};
alert(user); // John
alert(+user); // 30
alert(user + 1); // 31 (default like number calls valueOf)
alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500
```
Quite often we want a single "catch-all" place for primitive conversions, or only a nice debugging output of our object. In this case we can implement `toString` only. It the absence of `valueOf` it will be used for all conversions.
For instance:
In practice, we often want a single "catch-all" place to handle all primitive conversions. In this case we can implement `toString` only, like this:
```js run
let user = {
@ -149,21 +151,66 @@ let user = {
}
};
// here the hint would be "number"
// only toString exists, so it is used
alert( user > 'Abba' ); // true, "John" > "Abba"
alert( user < 'Zeta' ); // true, "John" < "Zeta"
alert(user); // toString -> John
alert(user + 500); // toString -> John500
```
````smart header="There's no `ToBoolean`"
There is no such thing as `ToBoolean`. All objects (even empty) are `true` in boolean context:
## ToPrimitive and ToString/ToNumber
```js run
if ({}) alert("true"); // works
```
The important thing to know about all primitive-conversion methods is that they not necessarily return the "hinted" primitive.
That is not customizable.
````
There is no control whether `toString()` returns exactly a string, or whether `Symbol.toPrimitive` method returns a number for a hint "number".
**The only mandatory thing: these methods must return a primitive.**
An operation that was the reason for the conversion gets that primitive, and then continues to work with it, applying further conversions if necessary.
For instance:
- All mathematical operations except binary plus apply `ToNumber`
```js run
let obj = {
toString() { // toString used for everything in the absense of other methods
return "2";
}
};
alert(obj * 2); // 4
```
- Binary plus first checks if it's a string, and then does concatenation, otherwise performs `ToNumber` and works with numbers.
```js run
let obj = {
toString() {
return "2";
}
};
alert(obj + 2); // 22 (string => concatenation)
```
```js run
let obj = {
toString() {
return true;
}
};
alert(obj + 2); // 3 (not string => ToNumber)
```
## Summary
[todo describe article]
Minor notes:
- If `Symbol.toPrimitive` returns an object, that's an error.
- If `toString/valueOf` return an object, they are ignored (historical behavior).
- By default, all objects have both `toString` and `valueOf`, but `valueOf` returns the object itself, and hence is ignored.