This commit is contained in:
Ilya Kantor 2019-08-04 17:36:26 +03:00
parent f96872425d
commit 6d1fa5de73
8 changed files with 84 additions and 79 deletions

View file

@ -224,6 +224,6 @@ MyClass.property = ...
MyClass.method = ...
```
Static properties are inherited.
Static properties and methods are inherited.
For `class B extends A` the prototype of the class `B` itself points to `A`: `B.[[Prototype]] = A`. So if a field is not found in `B`, the search continues in `A`.

View file

@ -53,7 +53,7 @@ In JavaScript, there are two types of object fields (properties and methods):
- Public: accessible from anywhere. They comprise the external interface. Till now we were only using public properties and methods.
- Private: accessible only from inside the class. These are for the internal interface.
In many other languages there also exist "protected" fields: accessible only from inside the class and those extending it. They are also useful for the internal interface. They are in a sense more widespread than private ones, because we usually want inheriting classes to gain access to them.
In many other languages there also exist "protected" fields: accessible only from inside the class and those extending it (like private, but plus access from inheriting classes). They are also useful for the internal interface. They are in a sense more widespread than private ones, because we usually want inheriting classes to gain access to them.
Protected fields are not implemented in JavaScript on the language level, but in practice they are very convenient, so they are emulated.
@ -297,7 +297,7 @@ Supportable
**If we strictly delimit the internal interface, then the developer of the class can freely change its internal properties and methods, even without informing the users.**
If you're a developer of such class, it's great to know that private methods can be safely renamed, their parameters can be changed, and even removed, because no external code depends on them.
If you're a developer of such class, it's great to know that private methods can be safely renamed, their parameters can be changed, and even removed, because no external code depends on them.
For users, when a new version comes out, it may be a total overhaul internally, but still simple to upgrade if the external interface is the same.

View file

@ -21,14 +21,14 @@ alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false
```
Please note a very interesting thing. Built-in methods like `filter`, `map` and others -- return new objects of exactly the inherited type. They rely on the `constructor` property to do so.
Please note a very interesting thing. Built-in methods like `filter`, `map` and others -- return new objects of exactly the inherited type `PowerArray`. Their internal implementation uses object `constructor` property for that.
In the example above,
```js
arr.constructor === PowerArray
```
So when `arr.filter()` is called, it internally creates the new array of results using exactly `new PowerArray`, not basic `Array`. That's actually very cool, because we can keep using `PowerArray` methods further on the result.
When `arr.filter()` is called, it internally creates the new array of results using exactly `arr.constructor`, not basic `Array`. That's actually very cool, because we can keep using `PowerArray` methods further on the result.
Even more, we can customize that behavior.
@ -64,6 +64,10 @@ alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function
As you can see, now `.filter` returns `Array`. So the extended functionality is not passed any further.
```smart header="Other collections work similarly"
Other collections, such as `Map` and `Set`, work alike. They also use `Symbol.species`.
```
## No static inheritance in built-ins
Built-in objects have their own static methods, for instance `Object.keys`, `Array.isArray` etc.
@ -81,3 +85,5 @@ Here's the picture structure for `Date` and `Object`:
![](object-date-inheritance.svg)
As you can see, there's no link between `Date` and `Object`. They are independent, only `Date.prototype` inherits from `Object.prototype`.
That's an important difference of inheritance between built-in objects compared to what we get with `extends`.

View file

@ -11,7 +11,7 @@ The syntax is:
obj instanceof Class
```
It returns `true` if `obj` belongs to the `Class` (or a class inheriting from it).
It returns `true` if `obj` belongs to the `Class` or a class inheriting from it.
For instance:
@ -46,15 +46,17 @@ alert( arr instanceof Object ); // true
Please note that `arr` also belongs to the `Object` class. That's because `Array` prototypally inherits from `Object`.
The `instanceof` operator examines the prototype chain for the check, but we can set a custom logic in the static method `Symbol.hasInstance`.
Normally, `instanceof` operator examines the prototype chain for the check. We can also set a custom logic in the static method `Symbol.hasInstance`.
The algorithm of `obj instanceof Class` works roughly as follows:
1. If there's a static method `Symbol.hasInstance`, then just call it: `Class[Symbol.hasInstance](obj)`. It should return either `true` or `false`. We're done.
For example:
1. If there's a static method `Symbol.hasInstance`, then just call it: `Class[Symbol.hasInstance](obj)`. It should return either `true` or `false`, and we're done. That's how we can customize the behavior of `instanceof`.
For example:
```js run
// setup instanceOf check that assumes that anything that canEat is an animal
// setup instanceOf check that assumes that
// anything with canEat property is an animal
class Animal {
static [Symbol.hasInstance](obj) {
if (obj.canEat) return true;
@ -68,17 +70,19 @@ The algorithm of `obj instanceof Class` works roughly as follows:
2. Most classes do not have `Symbol.hasInstance`. In that case, the standard logic is used: `obj instanceOf Class` checks whether `Class.prototype` equals to one of prototypes in the `obj` prototype chain.
In other words, compare:
In other words, compare one after another:
```js
obj.__proto__ === Class.prototype
obj.__proto__.__proto__ === Class.prototype
obj.__proto__.__proto__.__proto__ === Class.prototype
obj.__proto__ === Class.prototype?
obj.__proto__.__proto__ === Class.prototype?
obj.__proto__.__proto__.__proto__ === Class.prototype?
...
// if any answer is true, return true
// otherwise, if we reached the end of the chain, return false
```
In the example above `Rabbit.prototype === rabbit.__proto__`, so that gives the answer immediately.
In the example above `rabbit.__proto__ === Rabbit.prototype`, so that gives the answer immediately.
In the case of an inheritance, `rabbit` is an instance of the parent class as well:
In the case of an inheritance, the match will be at the second step:
```js run
class Animal {}
@ -88,8 +92,11 @@ The algorithm of `obj instanceof Class` works roughly as follows:
*!*
alert(rabbit instanceof Animal); // true
*/!*
// rabbit.__proto__ === Rabbit.prototype
*!*
// rabbit.__proto__.__proto__ === Animal.prototype (match!)
*/!*
```
Here's the illustration of what `rabbit instanceof Animal` compares with `Animal.prototype`:
@ -100,7 +107,7 @@ By the way, there's also a method [objA.isPrototypeOf(objB)](mdn:js/object/isPro
That's funny, but the `Class` constructor itself does not participate in the check! Only the chain of prototypes and `Class.prototype` matters.
That can lead to interesting consequences when `prototype` is changed.
That can lead to interesting consequences when `prototype` property is changed after the object is created.
Like here:
@ -117,8 +124,6 @@ alert( rabbit instanceof Rabbit ); // false
*/!*
```
That's one of the reasons to avoid changing `prototype`. Just to keep safe.
## Bonus: Object.prototype.toString for the type
We already know that plain objects are converted to string as `[object Object]`:
@ -152,7 +157,7 @@ let objectToString = Object.prototype.toString;
// what type is this?
let arr = [];
alert( objectToString.call(arr) ); // [object Array]
alert( objectToString.call(arr) ); // [object *!*Array*/!*]
```
Here we used [call](mdn:js/function/call) as described in the chapter [](info:call-apply-decorators) to execute the function `objectToString` in the context `this=arr`.
@ -196,11 +201,11 @@ As you can see, the result is exactly `Symbol.toStringTag` (if exists), wrapped
At the end we have "typeof on steroids" that not only works for primitive data types, but also for built-in objects and even can be customized.
It can be used instead of `instanceof` for built-in objects when we want to get the type as a string rather than just to check.
We can use `{}.toString.call` instead of `instanceof` for built-in objects when we want to get the type as a string rather than just to check.
## Summary
Let's recap the type-checking methods that we know:
Let's summarize the type-checking methods that we know:
| | works for | returns |
|---------------|-------------|---------------|

View file

@ -2,9 +2,9 @@
In JavaScript we can only inherit from a single object. There can be only one `[[Prototype]]` for an object. And a class may extend only one other class.
But sometimes that feels limiting. For instance, I have a class `StreetSweeper` and a class `Bicycle`, and want to make a `StreetSweepingBicycle`.
But sometimes that feels limiting. For instance, I have a class `StreetSweeper` and a class `Bicycle`, and want to make their mix: a `StreetSweepingBicycle`.
Or, talking about programming, we have a class `User` and a class `EventEmitter` that implements event generation, and we'd like to add the functionality of `EventEmitter` to `User`, so that our users can emit events.
Or we have a class `User` and a class `EventEmitter` that implements event generation, and we'd like to add the functionality of `EventEmitter` to `User`, so that our users can emit events.
There's a concept that can help here, called "mixins".
@ -14,7 +14,7 @@ In other words, a *mixin* provides methods that implement a certain behavior, bu
## A mixin example
The simplest way to make a mixin in JavaScript is to make an object with useful methods, so that we can easily merge them into a prototype of any class.
The simplest way to implement a mixin in JavaScript is to make an object with useful methods, so that we can easily merge them into a prototype of any class.
For instance here the mixin `sayHiMixin` is used to add some "speech" for `User`:
@ -75,10 +75,10 @@ let sayHiMixin = {
*!*
// call parent method
*/!*
super.say(`Hello ${this.name}`);
super.say(`Hello ${this.name}`); // (*)
},
sayBye() {
super.say(`Bye ${this.name}`);
super.say(`Bye ${this.name}`); // (*)
}
};
@ -95,11 +95,13 @@ Object.assign(User.prototype, sayHiMixin);
new User("Dude").sayHi(); // Hello Dude!
```
Please note that the call to the parent method `super.say()` from `sayHiMixin` looks for the method in the prototype of that mixin, not the class.
Please note that the call to the parent method `super.say()` from `sayHiMixin` (at lines labelled with `(*)`) looks for the method in the prototype of that mixin, not the class.
Here's the diagram (see the right part):
![](mixin-inheritance.svg)
That's because methods `sayHi` and `sayBye` were initially created in `sayHiMixin`. So their `[[HomeObject]]` internal property references `sayHiMixin`, as shown on the picture above.
That's because methods `sayHi` and `sayBye` were initially created in `sayHiMixin`. So even though they got copied, their `[[HomeObject]]` internal property references `sayHiMixin`, as shown on the picture above.
As `super` looks for parent methods in `[[HomeObject]].[[Prototype]]`, that means it searches `sayHiMixin.[[Prototype]]`, not `User.[[Prototype]]`.
@ -199,7 +201,7 @@ And `eventMixin` mixin makes it easy to add such behavior to as many classes as
*Mixin* -- is a generic object-oriented programming term: a class that contains methods for other classes.
Some other languages like e.g. Python allow to create mixins using multiple inheritance. JavaScript does not support multiple inheritance, but mixins can be implemented by copying methods into prototype.
Some other languages like allow multiple inheritance. JavaScript does not support multiple inheritance, but mixins can be implemented by copying methods into prototype.
We can use mixins as a way to augment a class by multiple behaviors, like event-handling as we have seen above.