changes
This commit is contained in:
parent
b6ed18e70d
commit
c9401b3104
15 changed files with 231 additions and 233 deletions
|
@ -4,9 +4,9 @@ importance: 5
|
|||
|
||||
# Working with prototype
|
||||
|
||||
Here's the code that creates a pair of object, then alters them.
|
||||
Here's the code that creates a pair of objects, then modifies them.
|
||||
|
||||
Which values will be shown in the process?
|
||||
Which values are shown in the process?
|
||||
|
||||
```js
|
||||
let animal = {
|
||||
|
@ -28,4 +28,4 @@ delete animal.jumps;
|
|||
alert( rabbit.jumps ); // ? (3)
|
||||
```
|
||||
|
||||
There should be 3 answers.
|
||||
There should be 3 answers.
|
||||
|
|
|
@ -4,7 +4,9 @@ importance: 5
|
|||
|
||||
# Searching algorithm
|
||||
|
||||
We have object:
|
||||
The task has two parts.
|
||||
|
||||
We have an object:
|
||||
|
||||
```js
|
||||
let head = {
|
||||
|
@ -25,9 +27,5 @@ let pockets = {
|
|||
};
|
||||
```
|
||||
|
||||
The task has two parts:
|
||||
|
||||
1. Use `__proto__` to assign prototypes in a way that any property lookup will follow the path: `pockets -> bed -> table -> head`.
|
||||
|
||||
For instance, `pockets.pen` should be `3` (found in `table`), and `bed.glasses` should be `1` (found in `head`).
|
||||
1. Use `__proto__` to assign prototypes in a way that any property lookup will follow the path: `pockets` -> `bed` -> `table` -> `head`. For instance, `pockets.pen` should be `3` (found in `table`), and `bed.glasses` should be `1` (found in `head`).
|
||||
2. Answer the question: is it faster to get `glasses` as `pocket.glasses` or `head.glasses`? Benchmark if needed.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
**The answer: `rabbit`.**
|
||||
|
||||
That's because `this` is an object before the dot, so `rabbit.eat()` naturally means `rabbit`.
|
||||
That's because `this` is an object before the dot, so `rabbit.eat()` modifies `rabbit`.
|
||||
|
||||
Property lookup and execution are two successive things. The method is found in the prototype, but then is run in the context of `rabbit`.
|
||||
Property lookup and execution are two different things.
|
||||
The method `rabbit.eat` is first found in the prototype, then executed with `this=rabbit`
|
||||
|
|
|
@ -6,7 +6,7 @@ importance: 5
|
|||
|
||||
We have `rabbit` inheriting from `animal`.
|
||||
|
||||
If we call `rabbit.eat()`, which object receives `full`: `animal` or `rabbit`?
|
||||
If we call `rabbit.eat()`, which object receives the `full` property: `animal` or `rabbit`?
|
||||
|
||||
```js
|
||||
let animal = {
|
||||
|
@ -21,4 +21,3 @@ let rabbit = {
|
|||
|
||||
rabbit.eat();
|
||||
```
|
||||
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
Let's look carefully at what's going on in the call `speedy.eat("apple")`.
|
||||
|
||||
1. The method `speedy.eat` is found in the prototype (`hamster`), but called in the context of `speedy`. So the value of `this` is correct.
|
||||
1. The method `speedy.eat` is found in the prototype (`=hamster`), then executed with `this=speedy` (the object before the dot).
|
||||
|
||||
2. Then `this.stomach.push()` needs to find `stomach` property and call `push` on it. It looks for `stomach` in `this` (`=speedy`), but nothing found.
|
||||
2. Then `this.stomach.push()` needs to find `stomach` property and call `push` on it. It looks for `stomach` in `this` (`=speedy`), but nothing found.
|
||||
|
||||
3. Then it follows the prototype chain and finds `stomach` in `hamster`.
|
||||
|
||||
4. Then it calls `push` on it, adding the food into *the stomach of the prototype*.
|
||||
|
||||
It turns out that all hamsters share a single stomach!
|
||||
So all hamsters share a single stomach!
|
||||
|
||||
Please note that it would not happen in case of a simple assignment `this.stomach=`:
|
||||
Every time the `stomach` is taken from the prototype, then `stomach.push` modifies it "at place".
|
||||
|
||||
Please note that such thing doesn't happen in case of a simple assignment `this.stomach=`:
|
||||
|
||||
```js run
|
||||
let hamster = {
|
||||
|
@ -24,9 +26,13 @@ let hamster = {
|
|||
}
|
||||
};
|
||||
|
||||
let speedy = { __proto__: hamster };
|
||||
let speedy = {
|
||||
__proto__: hamster
|
||||
};
|
||||
|
||||
let lazy = { __proto__: hamster };
|
||||
let lazy = {
|
||||
__proto__: hamster
|
||||
};
|
||||
|
||||
// Speedy one found the food
|
||||
speedy.eat("apple");
|
||||
|
@ -36,9 +42,9 @@ alert( speedy.stomach ); // apple
|
|||
alert( lazy.stomach ); // <nothing>
|
||||
```
|
||||
|
||||
Now all works fine, because `this.stomach=` does not perform a lookup of `stomach`. The value is written directly into `this` object. And for a method call `this.stomach.push`, the object is to be found first (in the prototype), then called, that's the difference.
|
||||
Now all works fine, because `this.stomach=` does not perform a lookup of `stomach`. The value is written directly into `this` object.
|
||||
|
||||
But more often, we can totally evade the problem by making sure that each hamster has his own stomach, explicitly:
|
||||
Also we can totally evade the problem by making sure that each hamster has his own stomach:
|
||||
|
||||
```js run
|
||||
let hamster = {
|
||||
|
@ -49,14 +55,14 @@ let hamster = {
|
|||
}
|
||||
};
|
||||
|
||||
let speedy = {
|
||||
let speedy = {
|
||||
__proto__: hamster,
|
||||
*!*
|
||||
stomach: []
|
||||
*/!*
|
||||
};
|
||||
|
||||
let lazy = {
|
||||
let lazy = {
|
||||
__proto__: hamster,
|
||||
*!*
|
||||
stomach: []
|
||||
|
@ -71,5 +77,4 @@ alert( speedy.stomach ); // apple
|
|||
alert( lazy.stomach ); // <nothing>
|
||||
```
|
||||
|
||||
As a common solution, all object properties, like `stomach` above, are usually written into each object. That prevents such problems. From the other hand, methods and primives can safely stay in prototypes.
|
||||
|
||||
As a common solution, all properties that describe the state of a particular object, like `stomach` above, are usually written into that object. That prevents such problems.
|
||||
|
|
|
@ -6,7 +6,6 @@ For instance, we have a `user` object with its properties and methods, and want
|
|||
|
||||
*Prototypal inheritance* is a language feature that helps in that.
|
||||
|
||||
|
||||
[cut]
|
||||
|
||||
## [[Prototype]]
|
||||
|
@ -15,7 +14,7 @@ In JavaScript, objects have a special hidden property `[[Prototype]]` (as named
|
|||
|
||||

|
||||
|
||||
That `[[Prototype]]` has a "magical" meaning. When we want to read a property from `object`, and it's missing, JavaScript automatically takes it from the prototype. In programming, such thing is called a "prototypal inheritance". Many cool language features and approaches are based on it.
|
||||
That `[[Prototype]]` has a "magical" meaning. When we want to read a property from `object`, and it's missing, JavaScript automatically takes it from the prototype. In programming, such thing is called "prototypal inheritance". Many cool language features and programming techniques are based on it.
|
||||
|
||||
The property `[[Prototype]]` is internal and hidden, but there are many ways to set it.
|
||||
|
||||
|
@ -34,9 +33,9 @@ rabbit.__proto__ = animal;
|
|||
*/!*
|
||||
```
|
||||
|
||||
Please note that `__proto__` is *not the same* as `[[Prototype]]`. That's a getter/setter for it. We'll talk about other ways of setting it later, as for now `__proto__` will do just fine.
|
||||
Please note that `__proto__` is *not the same* as `[[Prototype]]`. That's a getter/setter for it. We'll talk about other ways of setting it later, but for now `__proto__` will do just fine.
|
||||
|
||||
So now if we look for something in `rabbit` and it's missing, JavaScript automatically takes it from `animal`.
|
||||
If we look for a property in `rabbit`, and it's missing, JavaScript automatically takes it from `animal`.
|
||||
|
||||
For instance:
|
||||
|
||||
|
@ -54,14 +53,14 @@ rabbit.__proto__ = animal; // (*)
|
|||
|
||||
// we can find both properties in rabbit now:
|
||||
*!*
|
||||
alert( rabbit.eats ); // true
|
||||
alert( rabbit.eats ); // true (**)
|
||||
*/!*
|
||||
alert( rabbit.jumps ); // true
|
||||
```
|
||||
|
||||
Here the line `(*)` sets `animal` to be a prototype of `rabbit`.
|
||||
|
||||
Then, when `alert` tries to read property `rabbit.eats`, it can find it `rabbit`, so it follows the `[[Prototype]]` reference and finds it in `animal` (look from the bottom up):
|
||||
Then, when `alert` tries to read property `rabbit.eats` `(**)`, it's not in `rabbit`, so JavaScript follows the `[[Prototype]]` reference and finds it in `animal` (look from the bottom up):
|
||||
|
||||

|
||||
|
||||
|
@ -126,21 +125,25 @@ alert(longEar.jumps); // true (from rabbit)
|
|||
|
||||
There are actually only two limitations:
|
||||
|
||||
1. The references can't go in circles. JavaScript will throw an error if we try to assign `__proto__` in circle.
|
||||
1. The references can't go in circles. JavaScript will throw an error if we try to assign `__proto__` in a circle.
|
||||
2. The value of `__proto__` can be either an object or `null`. All other values (like primitives) are ignored.
|
||||
|
||||
Also it may be obvious, but still: there can be only one `[[Prototype]]`. An object may not inherit from two others.
|
||||
|
||||
## Read/write rules
|
||||
|
||||
The prototype is only used for reading properties, write/delete for data properties works directly with the object.
|
||||
The prototype is only used for reading properties.
|
||||
|
||||
For data properties (not getters/setters) write/delete operations work directly with the object.
|
||||
|
||||
In the example below, we assign its own `walk` method to `rabbit`:
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
eats: true,
|
||||
walk() { /* unused by rabbit, because (see below) it has its own */ }
|
||||
walk() {
|
||||
/* this method won't be used by rabbit */
|
||||
}
|
||||
};
|
||||
|
||||
let rabbit = {
|
||||
|
@ -160,18 +163,22 @@ Since now, `rabbit.walk()` call finds the method immediately in the object and e
|
|||
|
||||

|
||||
|
||||
The assignment handling is different for accessor properties, because these properties behave more like functions. For instance, check out `admin.fullName` property in the code below:
|
||||
For getters/setters -- if we read/write a property, they are looked up in the prototype and invoked.
|
||||
|
||||
For instance, check out `admin.fullName` property in the code below:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
surname: "Smith",
|
||||
|
||||
*!*
|
||||
set fullName(value) {
|
||||
[this.name, this.surname] = value.split(" ");
|
||||
}
|
||||
*/!*
|
||||
|
||||
get fullName() {
|
||||
return `${this.name} ${this.surname}`;
|
||||
}
|
||||
};
|
||||
|
||||
let admin = {
|
||||
|
@ -179,21 +186,13 @@ let admin = {
|
|||
isAdmin: true
|
||||
};
|
||||
|
||||
// setter triggers!
|
||||
*!*
|
||||
admin.fullName = "Alice Cooper";
|
||||
*/!*
|
||||
alert(admin.fullName); // John Smith (*)
|
||||
|
||||
alert(admin.name); // Alice
|
||||
alert(admin.surname); // Cooper
|
||||
// setter triggers!
|
||||
admin.fullName = "Alice Cooper"; // (**)
|
||||
```
|
||||
|
||||
Here in the line `(*)` the property `admin.fullName` has a setter in the prototype `user`. So it is not written into `admin`. Instead, the inherited setter is called.
|
||||
|
||||
So, the general rule would be:
|
||||
|
||||
1. For accessor properties use a setter (from the prototype chain).
|
||||
2. Otherwise assign directly to the object.
|
||||
Here in the line `(*)` the property `admin.fullName` has a getter in the prototype `user`, so it is called. And in the line `(**)` the property is has a setter in the prototype, so it is called.
|
||||
|
||||
## The value of "this"
|
||||
|
||||
|
@ -201,7 +200,7 @@ An interesting question may arise in the example above: what's the value of `thi
|
|||
|
||||
The answer is simple: `this` is not affected by prototypes at all.
|
||||
|
||||
**No matter where a method is found: in an object or its prototype. In a method call, `this` is always the object before the dot.**
|
||||
**No matter where the method is found: in an object or its prototype. In a method call, `this` is always the object before the dot.**
|
||||
|
||||
So, the setter actually uses `admin` as `this`, not `user`.
|
||||
|
||||
|
@ -240,14 +239,14 @@ The resulting picture:
|
|||
|
||||

|
||||
|
||||
If we had other objects like `bird`, `snake` etc inheriting from `animal`, they would also gain access to its methods. But `this` in each method would be the corresponding object, not `animal`.
|
||||
If we had other objects like `bird`, `snake` etc inheriting from `animal`, they would also gain access to methods of `animal`. But `this` in each method would be the corresponding object, evaluated at the call-time (before dot), not `animal`. So when we write data into `this`, it is stored into these objects.
|
||||
|
||||
In other words, methods are shared, but the state will be not.
|
||||
As a result, methods are shared, but the object state is not.
|
||||
|
||||
## Summary
|
||||
|
||||
- In JavaScript, all objects have a hidden `[[Prototype]]` property that's either another object or `null`.
|
||||
- We can use `obj.__proto__` to access it (there are other ways too, to be covered soon).
|
||||
- The object references by `[[Prototype]]` is called a "prototype".
|
||||
- The object referenced by `[[Prototype]]` is called a "prototype".
|
||||
- If we want to read a property of `obj` or call a method, and it doesn't exist, then JavaScript tries to find it in the prototype. Write/delete operations work directly on the object, they don't use the prototype (unless the property is actually a setter).
|
||||
- If we call `obj.method()`, and the `method` is taken from the prototype, `this` still references `obj`. So methods always work with the current objects even if they are inherited.
|
||||
- If we call `obj.method()`, and the `method` is taken from the prototype, `this` still references `obj`. So methods always work with the current object even if they are inherited.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue