This commit is contained in:
Ilya Kantor 2017-01-07 01:25:31 +01:00
parent ad499144a1
commit 6c9c2219ba
19 changed files with 91 additions and 54 deletions

View file

@ -667,30 +667,26 @@ There's a standard algorithm for deep cloning that handles the case above and mo
## Summary
[todo rewrite]
Objects are associative arrays with several special features.
- Property keys are either strings or symbols.
They store properties (key-value pairs), where:
- Property keys must be strings or symbols (usually strings).
- Values can be of any type.
Property access:
To access a property, we can use:
- The dot notation: `obj.property`.
- Square brackets notation `obj["property"]`. Square brackets allow to take the key from a variable, like `obj[varWithKey]`.
- Read/write uses the dot notation: `obj.property` or square brackets `obj["property"]/obj[varWithKey]`.
- The deletion is made via the `delete` operator.
- Existance check is made by the comparison vs `undefined` or via the `in` operator.
- Three forms of looping:
- `for(key in obj)` for the keys.
- `for(value of Object.values(obj))` for the values.
- `for([key,value] of Object.entries(obj))` for both.
Additional operators:
- To delete a property: `delete obj.prop`.
- To check if a property with the given key exists: `"key" in obj`.
- To iterate over an object: `for(let key in obj)` loop.
- Ordering:
- Integer properties in sorted order first, then strings in creation order, then symbols in creation order.
- To keep the order for numeric properties, we can prepend them with `+` to make them look like non-numeric.
Objects are assigned and copied by reference. In other words, a variable stores not the "object value", but a "reference" (address in memory) for the value. So copying such a variable or passing it as a function argument copies that reference, not the object. All operations via copied references (like adding/removed properties) are performed on the same single object.
- Objects are assigned and copied by reference.
To make a "real copy" (a clone) we can use `Object.assign` or [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep).
What we've just seen is called a "plain object", or just `Object`.
What we've studied in this chapter is called a "plain object", or just `Object`.
There are many other kinds of objects in Javascript:
@ -699,6 +695,6 @@ There are many other kinds of objects in Javascript:
- `Error` to store the information about an error.
- ...And so on.
Sometimes people say something like "Array type" or "Date type", but formally they are not types of their own, but belong to a single "object" data type. And they extend it in various ways.
They have their special features that we'll study later. Sometimes people say something like "Array type" or "Date type", but formally they are not types of their own, but belong to a single "object" data type. And they extend it in various ways.
Objects in JavaScript are very powerful. Here we've just scratched the surface of the topic that is really huge. We'll be closely working with objects and learning more about them in further parts of the tutorial.

View file

@ -226,6 +226,12 @@ Here we are not to judge whether this language design decision is good or bad. W
## Internals: Reference Type
```warn header="In-depth language feature"
This section covers advanced topic that may interest those who want to know Javascript better.
If you want to go on faster, it can be skipped or postponed.
```
An intricate method call can loose `this`, for instance:
```js run
@ -296,7 +302,8 @@ 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).
````warn header="Arrow functions do not have `this`"
## Arrow functions have no "this"
Arrow functions are special: they don't have "own" `this`. If we reference `this` from such function, it's taken from the outer "normal" function.
For instance, here `arrow()` uses `this` from the outer `user.sayHi()` method:
@ -313,13 +320,18 @@ let user = {
user.sayHi(); // Ilya
```
That's a special feature of arrow functions, it's useful when we actually do not want to have a separate `this`, but rather to take it from the outer context. Later in the chapter <info:arrow-functions> we'll dig more deeply into what's going on.
That's a special feature of arrow functions, it's useful when we actually do not want to have a separate `this`, but rather to take it from the outer context. Later in the chapter <info:arrow-functions> we'll dig more deeply into what's going on.
````
## Summary
[todo]
- Functions that are stored in object properties are called "methods".
- Methods allow objects to "act" like `object.doSomething()`.
- Methods can reference the object as `this`.
- Primitives except `null` and `undefined` provide many helpful methods. We plan to study those in the next chapters.
- Formally, these methods work via temporary objects, but JavaScript engines are very well tuned to optimize that internally, so they are not expensive to call.
The value of `this` is defined at run-time.
- When a function is declared, it may use `this`, but that `this` has no value until the function is called.
- That function can be copied between objects.
- When a function is called in the "method" syntax: `object.method()`, the value of `this` during the call is `object`.
Please note that arrow functions are special: they have no `this`. When `this` is accessed inside an arrow function -- it is taken from outside.

View file

@ -11,29 +11,31 @@ But we left a gap for objects. Now let's close it. And, in the process, we'll se
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.
The conversion of an object to primitive value (a number or a string) is a rare thing in practice.
That's because 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 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 and understand it too.
For objects, there's no to-boolean conversion, because all objects are `true` in a boolean context. So there are only string and numeric conversions.
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. For real stuff, the output is more complicated. That's why it is usually implemented with object methods like `user.format(...)` or even in more advanced ways.
For instance, numeric conversion happens in most mathematical functions. But do we run maths on an object as a whole? Rarely the case. It also occurs when we compare an object against a primitive: `user > 18`. But should we write like that? What such comparison actually means? Maybe we want to compare `18` against the age of the `user`? Then it would be more obvious to write `user.age > 18`. And it's easier to read and understand it too.
So, most of the time, it's more flexible and gives more readable code to explicitly get an object property or call a method than rely on the conversion.
As for the string conversion... Where does it occur? Usually, when we output an object like `alert(obj)`. But `alert` and similar ways to show an object are only used for debugging and logging purposes. For real stuff, the output is more complicated. It is usually implemented with special object methods like `user.format(...)` or in more advanced ways.
That said, there are still valid reasons why we should know how to-primitive conversion works.
Still, there are still valid reasons why we should know how to-primitive conversion works:
- Simple object-as-string output is useable sometimes.
- Simple object-as-string output (`alert` and alike) is useable sometimes.
- Many built-in objects implement their own to-primitive conversion, we need to know how to work with that.
- Sometimes an unexpected conversion happens, 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 sign that person understands Javascript if he knows type conversions well.
- The final one. There are quizzes and questions on interviews that rely on that knowledge. Looks like people think it's a good sign that person understands Javascript if he knows type conversions well.
## ToPrimitive
The algorithm of object-to-primitive conversion is called `ToPrimitive` in [the specification](https://tc39.github.io/ecma262/#sec-toprimitive).
When an object is used in the context where a primitive is required, it is converted to primitive using the `ToPrimitive` algorithm, thoroughly described in the [specification](https://tc39.github.io/ecma262/#sec-toprimitive)).
There are 3 types (also called "hints") of object-to-primitive conversion:
That algorithm joins all conversion cases and allows to customize them in a special object method.
When a built-in function (like `alert` or mathematical functions) or an operator (like an addition or substraction) get an object as their argument, they initiate the to-primitive conversion using one of 3 so-called "hints":
`"string"`
: For object-to-string conversions, like:
: When an operation expects a string, for object-to-string conversions, like:
```js
// output
@ -44,7 +46,7 @@ There are 3 types (also called "hints") of object-to-primitive conversion:
```
`"number"`
: For object-to-number conversions, like:
: When an operation expects a number, for object-to-number conversions, like:
```js
// explicit conversion
@ -59,33 +61,46 @@ There are 3 types (also called "hints") of object-to-primitive conversion:
```
`"default"`
: Occurs in rare cases where it's not clear what is desired.
: Occurs in rare cases when the operator is "not sure" what type to expect.
For instance:
For instance, binary plus `+` can work both with strings (concatenates them) and numbers (adds them).
Or when an object is compared with a string, number or a symbol using `==`, then also the `"default"` hint is used.
```js
// binary plus can work both with strings (concatenates) and numbers (adds)
// binary plus
let total = car1 + car2;
// obj == string, number or symbol also uses default
// obj == string/number/symbol
if (user == 1) { ... };
```
So, binary addition `+` and equality check `==` use this hint. Seems right, because they operate both with strings and numbers. Although, there's some inconsistency here. The greater/less operator `<>` can work with both strings and numbers too. Still, it uses "number" hint. That's for historical reasons.
Seems right to use that hint for binary addition `+` and equality check `==`, because they operate both on strings and numbers. Although, there's some inconsistency here. The greater/less operator `<>` can work with both strings and numbers too. Still, it uses "number" hint, not "default". That's for historical reasons.
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.
Please note -- there are only three conversions. That simple. 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.
Please note -- there are only three hints. That simple. 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.
To do the conversion, Javascript tries to find and call three object methods:
**To do the conversion, Javascript tries to find and call three object methods:**
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.
1. Call `obj[Symbol.toPrimitive](hint)` if the method exists,
2. Otherwise if hint is `"string"`
- try `obj.toString()` and `obj.valueOf()`, whatever exists.
3. Otherwise if hint is `"number"` or `"default"`
- try `obj.valueOf()` and `obj.toString()`, whatever exists.
## Symbol.toPrimitive
For instance, here `user` object implements the 1st method:
Let's start from the first method. There's a built-in symbol named `Symbol.toPrimitive` that should be used to name the conversion method, like this:
```js
obj[Symbol.toPrimitive] = function(hint) {
// return a primitive value
// hint = one of "string", "number", "default"
}
```
For instance, here `user` object implements it:
```js run
let user = {
@ -104,11 +119,12 @@ 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.
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
Methods `toString` and `valueOf` come from ancient times, that's why they are not symbols. They provide an alternative "old-style" way to implement the conversion.
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.
If there's no `Symbol.toPrimitive` then Javascript tries to find them and try in the order:
@ -205,14 +221,28 @@ For instance:
alert(obj + 2); // 3 (ToPrimitive returned boolean, not string => ToNumber)
```
```smart header="Minor notes"
- If `Symbol.toPrimitive` exists, it *must* return an primitive, otherwise there will be an error.
- Methods `toString` or `valueOf` *should* return a primitive: if any of them returns and object, then it is ignored (like if the method didn't exist). That's the historical behavior.
```
## Summary
The object-to-primitive conversion is called automatically by many built-in functions and operators that expect a primitive as a value.
[todo describe article]
There are 3 types (hints) of it:
- `"string"`
- `"number"`
- `"default"`
So, operations use the hint depending on what they expect to get. 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 it works the same way as `"number"`, so in practice the last two are often merged together.
Minor notes:
The conversion algorithm is:
- 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.
1. Call `obj[Symbol.toPrimitive](hint)` if the method exists,
2. Otherwise if hint is `"string"`
- try `obj.toString()` and `obj.valueOf()`, whatever exists.
3. Otherwise if hint is `"number"` or `"default"`
- try `obj.valueOf()` and `obj.toString()`, whatever exists.
In practice, it's often enough to implement only `obj.toString()` as a "catch-all" method for all conversions that returns a "human-readable" representation of an object, for logging or debugging purposes.

View file

@ -1,2 +1 @@
# Object basics
# Objects: the basics