improvement

This commit is contained in:
Ilya Kantor 2019-05-23 22:27:44 +03:00
parent 9f7235d4eb
commit df7800adda
6 changed files with 99 additions and 22 deletions

View file

@ -268,12 +268,12 @@ let rabbit = {
}; };
*!* *!*
// only own keys // Object.keys only return own keys
alert(Object.keys(rabbit)); // jumps alert(Object.keys(rabbit)); // jumps
*/!* */!*
*!* *!*
// inherited keys too // for..in loops over both own and inherited keys
for(let prop in rabbit) alert(prop); // jumps, then eats for(let prop in rabbit) alert(prop); // jumps, then eats
*/!* */!*
``` ```
@ -294,17 +294,24 @@ let rabbit = {
for(let prop in rabbit) { for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop); let isOwn = rabbit.hasOwnProperty(prop);
alert(`${prop}: ${isOwn}`); // jumps: true, then eats: false
if (isOwn) {
alert(`Our: ${prop}`); // Our: jumps
} else {
alert(`Inherited: ${prop}`); // Inherited: eats
}
} }
``` ```
Here we have the following inheritance chain: `rabbit`, then `animal`, then `Object.prototype` (because `animal` is a literal object `{...}`, so it's by default), and then `null` above it: Here we have the following inheritance chain: `rabbit` inherits from `animal`, that inherits from `Object.prototype` (because `animal` is a literal object `{...}`, so it's by default), and then `null` above it:
![](rabbit-animal-object.png) ![](rabbit-animal-object.png)
Note, there's one funny thing. Where is the method `rabbit.hasOwnProperty` coming from? Looking at the chain we can see that the method is provided by `Object.prototype.hasOwnProperty`. In other words, it's inherited. Note, there's one funny thing. Where is the method `rabbit.hasOwnProperty` coming from? We did not define it. Looking at the chain we can see that the method is provided by `Object.prototype.hasOwnProperty`. In other words, it's inherited.
...But why `hasOwnProperty` does not appear in `for..in` loop, if it lists all inherited properties? The answer is simple: it's not enumerable. Just like all other properties of `Object.prototype`. That's why they are not listed. ...But why `hasOwnProperty` does not appear in `for..in` loop, like `eats` and `jumps`, if it lists all inherited properties.
The answer is simple: it's not enumerable. Just like all other properties of `Object.prototype`, it has `enumerable:false` flag. That's why they are not listed.
```smart header="All other iteration methods ignore inherited properties" ```smart header="All other iteration methods ignore inherited properties"
All other key/value-getting methods, such as `Object.keys`, `Object.values` and so on ignore inherited properties. All other key/value-getting methods, such as `Object.keys`, `Object.values` and so on ignore inherited properties.
@ -317,6 +324,7 @@ They only operate on the object itself. Properties from the prototype are taken
- In JavaScript, all objects have a hidden `[[Prototype]]` property that's either another object or `null`. - In JavaScript, all objects have a hidden `[[Prototype]]` property that's either another object or `null`.
- We can use `obj.__proto__` to access it (a historical getter/setter, there are other ways, to be covered soon). - We can use `obj.__proto__` to access it (a historical getter/setter, there are other ways, to be covered soon).
- The object referenced 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 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 for act directly on the object, they don't use the prototype (assuming it's a data property, not is 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 object 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.
- The `for..in` loop iterates over both own and inherited properties. All other key/value-getting methods only operate on the object itself. - The `for..in` loop iterates over both own and inherited properties. All other key/value-getting methods only operate on the object itself.

View file

@ -311,9 +311,9 @@ Let's get a little deeper under the hood of `super`. We'll see some interesting
First to say, from all that we've learned till now, it's impossible for `super` to work at all! First to say, from all that we've learned till now, it's impossible for `super` to work at all!
Yeah, indeed, let's ask ourselves, how it could technically work? When an object method runs, it gets the current object as `this`. If we call `super.method()` then, it needs to retrieve the `method` from the prototype of the current object. How JavaScript engine should get the prototype of `this`? Yeah, indeed, let's ask ourselves, how it could technically work? When an object method runs, it gets the current object as `this`. If we call `super.method()` then, it needs to retrieve the `method` from the prototype of the current object.
The task may seem simple, but it isn't. The engine could try to get the method from `[[Prototype]]` of `this`, as `this.__proto__.method`. Unfortunately, that doesn't work. The task may seem simple, but it isn't. The engine knows the current object `this`, so it could get the parent `method` as `this.__proto__.method`. Unfortunately, such a "naive" solution won't work.
Let's demonstrate the problem. Without classes, using plain objects for the sake of simplicity. Let's demonstrate the problem. Without classes, using plain objects for the sake of simplicity.
@ -414,18 +414,16 @@ The problem can't be solved by using `this` alone.
To provide the solution, JavaScript adds one more special internal property for functions: `[[HomeObject]]`. To provide the solution, JavaScript adds one more special internal property for functions: `[[HomeObject]]`.
**When a function is specified as a class or object method, its `[[HomeObject]]` property becomes that object.** When a function is specified as a class or object method, its `[[HomeObject]]` property becomes that object.
This actually violates the idea of "unbound" functions, because methods remember their objects. And `[[HomeObject]]` can't be changed, so this bound is forever. So that's a very important change in the language. Then `super` uses it to resolve the parent prototype and its methods.
But this change is safe. `[[HomeObject]]` is used only for calling parent methods in `super`, to resolve the prototype. So it doesn't break compatibility. Regular method calls know nothing about `[[HomeObject]]`, it only matters for `super`. Let's see how it works, first with plain objects:
Let's see how it works for `super` -- again, using plain objects:
```js run ```js run
let animal = { let animal = {
name: "Animal", name: "Animal",
eat() { // [[HomeObject]] == animal eat() { // animal.eat.[[HomeObject]] == animal
alert(`${this.name} eats.`); alert(`${this.name} eats.`);
} }
}; };
@ -433,7 +431,7 @@ let animal = {
let rabbit = { let rabbit = {
__proto__: animal, __proto__: animal,
name: "Rabbit", name: "Rabbit",
eat() { // [[HomeObject]] == rabbit eat() { // rabbit.eat.[[HomeObject]] == rabbit
super.eat(); super.eat();
} }
}; };
@ -441,19 +439,75 @@ let rabbit = {
let longEar = { let longEar = {
__proto__: rabbit, __proto__: rabbit,
name: "Long Ear", name: "Long Ear",
eat() { // [[HomeObject]] == longEar eat() { // longEar.eat.[[HomeObject]] == longEar
super.eat(); super.eat();
} }
}; };
*!* *!*
// works correctly
longEar.eat(); // Long Ear eats. longEar.eat(); // Long Ear eats.
*/!* */!*
``` ```
Every method remembers its object in the internal `[[HomeObject]]` property. Then `super` uses it to resolve the parent prototype. It works as intended, due to `[[HomeObject]]` mechanics. A method, such as `longEar.eat`, knows its `[[HomeObject]]` and takes the parent method from its prototype. Without any use of `this`.
`[[HomeObject]]` is defined for methods defined both in classes and in plain objects. But for objects, methods must be specified exactly the given way: as `method()`, not as `"method: function()"`. ### Methods are not "free"
As we've known before, generally functions are "free", not bound to objects in JavaScript. So they can be copied between objects and called with another `this`.
The very existance of `[[HomeObject]]` violates that principle, because methods remember their objects. `[[HomeObject]]` can't be changed, so this bond is forever.
The only place in the language where `[[HomeObject]]` is used -- is `super`. So, if a method does not use `super`, then we can still consider it free and copy between objects. But with `super` things may go wrong.
Here's the demo of a wrong `super` call:
```js run
let animal = {
sayHi() {
console.log(`I'm an animal`);
}
};
let rabbit = {
__proto__: animal,
sayHi() {
super.sayHi();
}
};
let plant = {
sayHi() {
console.log("I'm a plant");
}
};
let tree = {
__proto__: plant,
*!*
sayHi: rabbit.sayHi // (*)
*/!*
};
*!*
tree.sayHi(); // I'm an animal (?!?)
*/!*
```
A call to `tree.sayHi()` shows "I'm an animal". Definitevely wrong.
The reason is simple:
- In the line `(*)`, the method `tree.sayHi` was copied from `rabbit`. Maybe we just wanted to avoid code duplication?
- So its `[[HomeObject]]` is `rabbit`, as it was created in `rabbit`. There's no way to change `[[HomeObject]]`.
- The code of `tree.sayHi()` has `super.sayHi()` inside. It goes up from `rabbit` and takes the method from `animal`.
![](super-homeobject-wrong.png)
### Methods, not function properties
`[[HomeObject]]` is defined for methods both in classes and in plain objects. But for objects, methods must be specified exactly as `method()`, not as `"method: function()"`.
The difference may be non-essential for us, but it's important for JavaScript.
In the example below a non-method syntax is used for comparison. `[[HomeObject]]` property is not set and the inheritance doesn't work: In the example below a non-method syntax is used for comparison. `[[HomeObject]]` property is not set and the inheritance doesn't work:
@ -475,3 +529,18 @@ let rabbit = {
rabbit.eat(); // Error calling super (because there's no [[HomeObject]]) rabbit.eat(); // Error calling super (because there's no [[HomeObject]])
*/!* */!*
``` ```
## Summary
1. To extend a class: `class Child extends Parent`:
- That means `Child.prototype.__proto__` will be `Parent.prototype`, so methods are inherited.
2. When overriding a constructor:
- We must call parent constructor as `super()` in `Child` constructor before using `this`.
3. When overriding another method:
- We can use `super.method()` in a `Child` method to call `Parent` method.
4. Internals:
- Methods remember their class/object in the internal `[[HomeObject]]` property. That's how `super` resolves parent methods.
- So it's not safe to copy a method with `super` from one object to another.
Also:
- Arrow functions don't have own `this` or `super`, so they transparently fit into the surrounding context.

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -36,8 +36,8 @@ Then after any changes, the `callback` is executed, with a list of [MutationReco
- `type` -- mutation type, one of - `type` -- mutation type, one of
- `"attributes"`: attribute modified - `"attributes"`: attribute modified
- `"characterData"`: data modified - `"characterData"`: data modified, used for text nodes,
- `"childList"`: elements added/removed, - `"childList"`: child elements added/removed,
- `target` -- where the change occured: an element for "attributes", or text node for "characterData", or an element for a "childList" mutation, - `target` -- where the change occured: an element for "attributes", or text node for "characterData", or an element for a "childList" mutation,
- `addedNodes/removedNodes` -- nodes that were added/removed, - `addedNodes/removedNodes` -- nodes that were added/removed,
- `previousSibling/nextSibling` -- the previous and next sibling to added/removed nodes, - `previousSibling/nextSibling` -- the previous and next sibling to added/removed nodes,

View file

@ -3,7 +3,7 @@
The lifecycle of an HTML page has three important events: The lifecycle of an HTML page has three important events:
- `DOMContentLoaded` -- the browser fully loaded HTML, and the DOM tree is built, but external resources like pictures `<img>` and stylesheets may be not yet loaded. - `DOMContentLoaded` -- the browser fully loaded HTML, and the DOM tree is built, but external resources like pictures `<img>` and stylesheets may be not yet loaded.
- `load` -- not onyl HTML is loaded, but also all the external resources: images, styles etc. - `load` -- not only HTML is loaded, but also all the external resources: images, styles etc.
- `beforeunload/unload` -- the user is leaving the page. - `beforeunload/unload` -- the user is leaving the page.
Each event may be useful: Each event may be useful:

Binary file not shown.