refactor classes, add private, minor fixes
|
@ -0,0 +1,46 @@
|
|||
Here's the line with the error:
|
||||
|
||||
```js
|
||||
Rabbit.prototype = Animal.prototype;
|
||||
```
|
||||
|
||||
Here `Rabbit.prototype` and `Animal.prototype` become the same object. So methods of both classes become mixed in that object.
|
||||
|
||||
As a result, `Rabbit.prototype.walk` overwrites `Animal.prototype.walk`, so all animals start to bounce:
|
||||
|
||||
```js run
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Animal.prototype.walk = function() {
|
||||
alert(this.name + ' walks');
|
||||
};
|
||||
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
*!*
|
||||
Rabbit.prototype = Animal.prototype;
|
||||
*/!*
|
||||
|
||||
Rabbit.prototype.walk = function() {
|
||||
alert(this.name + " bounces!");
|
||||
};
|
||||
|
||||
*!*
|
||||
let animal = new Animal("pig");
|
||||
animal.walk(); // pig bounces!
|
||||
*/!*
|
||||
```
|
||||
|
||||
The correct variant would be:
|
||||
|
||||
```js
|
||||
Rabbit.prototype.__proto__ = Animal.prototype;
|
||||
// or like this:
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
```
|
||||
|
||||
That makes prototypes separate, each of them stores methods of the corresponding class, but `Rabbit.prototype` inherits from `Animal.prototype`.
|
|
@ -0,0 +1,29 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# An error in the inheritance
|
||||
|
||||
Find an error in the prototypal inheritance below.
|
||||
|
||||
What's wrong? What are consequences going to be?
|
||||
|
||||
```js
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Animal.prototype.walk = function() {
|
||||
alert(this.name + ' walks');
|
||||
};
|
||||
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Rabbit.prototype = Animal.prototype;
|
||||
|
||||
Rabbit.prototype.walk = function() {
|
||||
alert(this.name + " bounces!");
|
||||
};
|
||||
```
|
|
@ -0,0 +1 @@
|
|||
Please note that properties that were internal in functional style (`template`, `timer`) and the internal method `render` are marked private with the underscore `_`.
|
|
@ -0,0 +1,32 @@
|
|||
function Clock({ template }) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
Clock.prototype.render = function() {
|
||||
let date = new Date();
|
||||
|
||||
let hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
let mins = date.getMinutes();
|
||||
if (mins < 10) mins = '0' + mins;
|
||||
|
||||
let secs = date.getSeconds();
|
||||
if (secs < 10) secs = '0' + secs;
|
||||
|
||||
let output = this.template
|
||||
.replace('h', hours)
|
||||
.replace('m', mins)
|
||||
.replace('s', secs);
|
||||
|
||||
console.log(output);
|
||||
};
|
||||
|
||||
Clock.prototype.stop = function() {
|
||||
clearInterval(this.timer);
|
||||
};
|
||||
|
||||
Clock.prototype.start = function() {
|
||||
this.render();
|
||||
this.timer = setInterval(() => this.render(), 1000);
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Console clock</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="clock.js"></script>
|
||||
<script>
|
||||
let clock = new Clock({template: 'h:m:s'});
|
||||
clock.start();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,34 @@
|
|||
function Clock({ template }) {
|
||||
|
||||
let timer;
|
||||
|
||||
function render() {
|
||||
let date = new Date();
|
||||
|
||||
let hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
let mins = date.getMinutes();
|
||||
if (mins < 10) mins = '0' + mins;
|
||||
|
||||
let secs = date.getSeconds();
|
||||
if (secs < 10) secs = '0' + secs;
|
||||
|
||||
let output = template
|
||||
.replace('h', hours)
|
||||
.replace('m', mins)
|
||||
.replace('s', secs);
|
||||
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
this.stop = function() {
|
||||
clearInterval(timer);
|
||||
};
|
||||
|
||||
this.start = function() {
|
||||
render();
|
||||
timer = setInterval(render, 1000);
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Console clock</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="clock.js"></script>
|
||||
<script>
|
||||
let clock = new Clock({template: 'h:m:s'});
|
||||
clock.start();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Rewrite to prototypes
|
||||
|
||||
The `Clock` class is written in functional style. Rewrite it using prototypes.
|
||||
|
||||
P.S. The clock ticks in the console, open it to see.
|
240
1-js/09-classes/01-class-patterns/article.md
Normal file
|
@ -0,0 +1,240 @@
|
|||
|
||||
# Class patterns
|
||||
|
||||
```quote author="Wikipedia"
|
||||
In object-oriented programming, a *class* is an extensible program-code-template for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions or methods).
|
||||
```
|
||||
|
||||
There's a special syntax construct and a keyword `class` in JavaScript. But before studying it, we should consider that the term "class" comes from the theory of object-oriented programming. The definition is cited above, and it's language-independent.
|
||||
|
||||
In JavaScript there are several well-known programming patterns to make classes even without using the `class` keyword. People talk about "classes" meaning no only those defined with `class`, but also with these patterns.
|
||||
|
||||
The `class` construct will be described in the next chapter, but in JavaScript it's a "syntax sugar" and an extension of the prototypal class pattern described here.
|
||||
|
||||
|
||||
## Functional class pattern
|
||||
|
||||
The constructor function below can be considered a "class" according to the definition:
|
||||
|
||||
```js run
|
||||
function User(name) {
|
||||
this.sayHi = function() {
|
||||
alert(name);
|
||||
};
|
||||
}
|
||||
|
||||
let user = new User("John");
|
||||
user.sayHi(); // John
|
||||
```
|
||||
|
||||
It follows all parts of the definition:
|
||||
|
||||
1. It is a "program-code-template" for creating objects (callable with `new`).
|
||||
2. It provides initial values for the state (`name` from parameters).
|
||||
3. It provides methods (`sayHi`).
|
||||
|
||||
This is called *functional class pattern*.
|
||||
|
||||
In the functional class pattern, local variables and nested functions inside `User`, that are not assigned to `this`, are visible from inside, but not accessible by the outer code.
|
||||
|
||||
So we can easily add internal functions and variables, like `calcAge()` here:
|
||||
|
||||
```js run
|
||||
function User(name, birthday) {
|
||||
*!*
|
||||
// only visible from other methods inside User
|
||||
function calcAge() {
|
||||
return new Date().getFullYear() - birthday.getFullYear();
|
||||
}
|
||||
*/!*
|
||||
|
||||
this.sayHi = function() {
|
||||
alert(`${name}, age:${calcAge()}`);
|
||||
};
|
||||
}
|
||||
|
||||
let user = new User("John", new Date(2000, 0, 1));
|
||||
user.sayHi(); // John, age:17
|
||||
```
|
||||
|
||||
In this code variables `name`, `birthday` and the function `calcAge()` are internal, *private* to the object. They are only visible from inside of it.
|
||||
|
||||
On the other hand, `sayHi` is the external, *public* method. The external code that creates `user` can access it.
|
||||
|
||||
This way we can hide internal implementation details and helper methods from the outer code. Only what's assigned to `this` becomes visible outside.
|
||||
|
||||
## Factory class pattern
|
||||
|
||||
We can create a class without using `new` at all.
|
||||
|
||||
Like this:
|
||||
|
||||
```js run
|
||||
function User(name, birthday) {
|
||||
// only visible from other methods inside User
|
||||
function calcAge() {
|
||||
return new Date().getFullYear() - birthday.getFullYear();
|
||||
}
|
||||
|
||||
return {
|
||||
sayHi() {
|
||||
alert(`${name}, age:${calcAge()}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
*!*
|
||||
let user = User("John", new Date(2000, 0, 1));
|
||||
*/!*
|
||||
user.sayHi(); // John, age:17
|
||||
```
|
||||
|
||||
As we can see, the function `User` returns an object with public properties and methods. The only benefit of this method is that we can omit `new`: write `let user = User(...)` instead of `let user = new User(...)`. In other aspects it's almost the same as the functional pattern.
|
||||
|
||||
## Prototype-based classes
|
||||
|
||||
Prototype-based classes are the most important and generally the best. Functional and factory class patterns are rarely used in practice.
|
||||
|
||||
Soon you'll see why.
|
||||
|
||||
Here's the same class rewritten using prototypes:
|
||||
|
||||
```js run
|
||||
function User(name, birthday) {
|
||||
*!*
|
||||
this._name = name;
|
||||
this._birthday = birthday;
|
||||
*/!*
|
||||
}
|
||||
|
||||
*!*
|
||||
User.prototype._calcAge = function() {
|
||||
*/!*
|
||||
return new Date().getFullYear() - this._birthday.getFullYear();
|
||||
};
|
||||
|
||||
User.prototype.sayHi = function() {
|
||||
alert(`${this._name}, age:${this._calcAge()}`);
|
||||
};
|
||||
|
||||
let user = new User("John", new Date(2000, 0, 1));
|
||||
user.sayHi(); // John, age:17
|
||||
```
|
||||
|
||||
The code structure:
|
||||
|
||||
- The constructor `User` only initializes the current object state.
|
||||
- Methods are added to `User.prototype`.
|
||||
|
||||
As we can see, methods are lexically not inside `function User`, they do not share a common lexical environment. If we declare variables inside `function User`, then they won't be visible to methods.
|
||||
|
||||
So, there is a widely known agreement that internal properties and methods are prepended with an underscore `"_"`. Like `_name` or `_calcAge()`. Technically, that's just an agreement, the outer code still can access them. But most developers recognize the meaning of `"_"` and try not to touch prefixed properties and methods in the external code.
|
||||
|
||||
Here are the advantages over the functional pattern:
|
||||
|
||||
- In the functional pattern, each object has its own copy of every method. We assign a separate copy of `this.sayHi = function() {...}` and other methods in the constructor.
|
||||
- In the prototypal pattern, all methods are in `User.prototype` that is shared between all user objects. An object itself only stores the data.
|
||||
|
||||
So the prototypal pattern is more memory-efficient.
|
||||
|
||||
...But not only that. Prototypes allow us to setup the inheritance in a really efficient way. Built-in JavaScript objects all use prototypes. Also there's a special syntax construct: "class" that provides nice-looking syntax for them. And there's more, so let's go on with them.
|
||||
|
||||
## Prototype-based inheritance for classes
|
||||
|
||||
Let's say we have two prototype-based classes.
|
||||
|
||||
`Rabbit`:
|
||||
|
||||
```js
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Rabbit.prototype.jump = function() {
|
||||
alert(`${this.name} jumps!`);
|
||||
};
|
||||
|
||||
let rabbit = new Rabbit("My rabbit");
|
||||
```
|
||||
|
||||

|
||||
|
||||
...And `Animal`:
|
||||
|
||||
```js
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Animal.prototype.eat = function() {
|
||||
alert(`${this.name} eats.`);
|
||||
};
|
||||
|
||||
let animal = new Animal("My animal");
|
||||
```
|
||||
|
||||

|
||||
|
||||
Right now they are fully independent.
|
||||
|
||||
But we'd want `Rabbit` to extend `Animal`. In other words, rabbits should be based on animals, have access to methods of `Animal` and extend them with its own methods.
|
||||
|
||||
What does it mean in the language of prototypes?
|
||||
|
||||
Right now methods for `rabbit` objects are in `Rabbit.prototype`. We'd like `rabbit` to use `Animal.prototype` as a "fallback", if the method is not found in `Rabbit.prototype`.
|
||||
|
||||
So the prototype chain should be `rabbit` -> `Rabbit.prototype` -> `Animal.prototype`.
|
||||
|
||||
Like this:
|
||||
|
||||

|
||||
|
||||
The code to implement that:
|
||||
|
||||
```js run
|
||||
// Same Animal as before
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
// All animals can eat, right?
|
||||
Animal.prototype.eat = function() {
|
||||
alert(`${this.name} eats.`);
|
||||
};
|
||||
|
||||
// Same Rabbit as before
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Rabbit.prototype.jump = function() {
|
||||
alert(`${this.name} jumps!`);
|
||||
};
|
||||
|
||||
*!*
|
||||
// setup the inheritance chain
|
||||
Rabbit.prototype.__proto__ = Animal.prototype; // (*)
|
||||
*/!*
|
||||
|
||||
let rabbit = new Rabbit("White Rabbit");
|
||||
*!*
|
||||
rabbit.eat(); // rabbits can eat too
|
||||
*/!*
|
||||
rabbit.jump();
|
||||
```
|
||||
|
||||
The line `(*)` sets up the prototype chain. So that `rabbit` first searches methods in `Rabbit.prototype`, then `Animal.prototype`. And then, just for completeness, let's mention that if the method is not found in `Animal.prototype`, then the search continues in `Object.prototype`, because `Animal.prototype` is a regular plain object, so it inherits from it.
|
||||
|
||||
So here's the full picture:
|
||||
|
||||

|
||||
|
||||
## Summary
|
||||
|
||||
The term "class" comes from the object-oriented programming. In JavaScript it usually means the functional class pattern or the prototypal pattern. The prototypal pattern is more powerful and memory-efficient, so it's recommended to stick to it.
|
||||
|
||||
According to the prototypal pattern:
|
||||
1. Methods are stored in `Class.prototype`.
|
||||
2. Prototypes inherit from each other.
|
||||
|
||||
In the next chapter we'll study `class` keyword and construct. It allows to write prototypal classes shorter and provides some additional benefits.
|
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 37 KiB |
0
1-js/09-classes/02-class/1-rewrite-to-class/solution.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
class Clock {
|
||||
constructor({ template }) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
render() {
|
||||
let date = new Date();
|
||||
|
||||
let hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
let mins = date.getMinutes();
|
||||
if (mins < 10) mins = '0' + mins;
|
||||
|
||||
let secs = date.getSeconds();
|
||||
if (secs < 10) secs = '0' + secs;
|
||||
|
||||
let output = this.template
|
||||
.replace('h', hours)
|
||||
.replace('m', mins)
|
||||
.replace('s', secs);
|
||||
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.render();
|
||||
this.timer = setInterval(() => this.render(), 1000);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Console clock</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="clock.js"></script>
|
||||
<script>
|
||||
let clock = new Clock({template: 'h:m:s'});
|
||||
clock.start();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,34 @@
|
|||
|
||||
|
||||
function Clock({ template }) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
Clock.prototype.render = function() {
|
||||
let date = new Date();
|
||||
|
||||
let hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
let mins = date.getMinutes();
|
||||
if (mins < 10) mins = '0' + mins;
|
||||
|
||||
let secs = date.getSeconds();
|
||||
if (secs < 10) secs = '0' + secs;
|
||||
|
||||
let output = this.template
|
||||
.replace('h', hours)
|
||||
.replace('m', mins)
|
||||
.replace('s', secs);
|
||||
|
||||
console.log(output);
|
||||
};
|
||||
|
||||
Clock.prototype.stop = function() {
|
||||
clearInterval(this.timer);
|
||||
};
|
||||
|
||||
Clock.prototype.start = function() {
|
||||
this.render();
|
||||
this.timer = setInterval(() => this.render(), 1000);
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Console clock</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="clock.js"></script>
|
||||
<script>
|
||||
let clock = new Clock({template: 'h:m:s'});
|
||||
clock.start();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
9
1-js/09-classes/02-class/1-rewrite-to-class/task.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Rewrite to class
|
||||
|
||||
Rewrite the `Clock` class from prototypes to the modern "class" syntax.
|
||||
|
||||
P.S. The clock ticks in the console, open it to see.
|
272
1-js/09-classes/02-class/article.md
Normal file
|
@ -0,0 +1,272 @@
|
|||
|
||||
# Classes
|
||||
|
||||
The "class" construct allows to define prototype-based classes with a clean, nice-looking syntax. It also introduces new great features, useful for object-oriented programming.
|
||||
|
||||
## The "class" syntax
|
||||
|
||||
The `class` syntax is versatile, we'll start with a simple example first.
|
||||
|
||||
Here's a prototype-based class `User`:
|
||||
|
||||
```js run
|
||||
function User(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
User.prototype.sayHi = function() {
|
||||
alert(this.name);
|
||||
}
|
||||
|
||||
let user = new User("John");
|
||||
user.sayHi();
|
||||
```
|
||||
|
||||
...And that's the same using `class` syntax:
|
||||
|
||||
```js run
|
||||
class User {
|
||||
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
sayHi() {
|
||||
alert(this.name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let user = new User("John");
|
||||
user.sayHi();
|
||||
```
|
||||
|
||||
It's easy to see that the two examples are alike. Just please note that methods in a class do not have a comma between them. Novice developers sometimes forget it and put a comma between class methods, and things don't work. That's not a literal object, but a class syntax.
|
||||
|
||||
## What is a class?
|
||||
|
||||
So, what exactly is a `class`? We may think that it defines a new language-level entity, but that would be wrong.
|
||||
|
||||
In Javascript, a class is a kind of a function.
|
||||
|
||||
The definition `class User {...}` create such function and puts the methods into `User.prototype`. So the structure is similar.
|
||||
|
||||
Here's the code to dig into the class and see that:
|
||||
|
||||
```js run
|
||||
class User {
|
||||
constructor(name) { this.name = name; }
|
||||
sayHi() { alert(this.name); }
|
||||
}
|
||||
|
||||
*!*
|
||||
// proof: User is a function
|
||||
alert(typeof User); // function
|
||||
*/!*
|
||||
|
||||
*!*
|
||||
// proof: User is the "constructor" function
|
||||
*/!*
|
||||
alert(User === User.prototype.constructor); // true
|
||||
|
||||
*!*
|
||||
// proof: there are two methods in its "prototype"
|
||||
*/!*
|
||||
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
|
||||
```
|
||||
|
||||
Here's the illustration of what `class User` creates:
|
||||
|
||||

|
||||
|
||||
So `class` is a special syntax to define a constructor together with its prototype methods. There are also quite a few additional features here and there, we'll study them later on.
|
||||
|
||||
## Class Expression
|
||||
|
||||
Just like functions, classes can be defined inside another expression, passed around, returned etc.
|
||||
|
||||
Here's a class-returning function ("class factory"):
|
||||
|
||||
```js run
|
||||
function makeClass(phrase) {
|
||||
*!*
|
||||
// declare a class and return it
|
||||
return class {
|
||||
sayHi() {
|
||||
alert(phrase);
|
||||
};
|
||||
};
|
||||
*/!*
|
||||
}
|
||||
|
||||
let User = makeClass("Hello");
|
||||
|
||||
new User().sayHi(); // Hello
|
||||
```
|
||||
|
||||
That's quite normal if we recall that `class` is just a special form of a function-with-prototype definition.
|
||||
|
||||
And, like Named Function Expressions, such classes also may have a name, that is visible inside that class only:
|
||||
|
||||
```js run
|
||||
// "Named Class Expression" (alas, no such term, but that's what's going on)
|
||||
let User = class *!*MyClass*/!* {
|
||||
sayHi() {
|
||||
alert(MyClass); // MyClass is visible only inside the class
|
||||
}
|
||||
};
|
||||
|
||||
new User().sayHi(); // works, shows MyClass definition
|
||||
|
||||
alert(MyClass); // error, MyClass not visible outside of the class
|
||||
```
|
||||
|
||||
## Differences of classes vs functions
|
||||
|
||||
Classes have some differences compared to regular functions:
|
||||
|
||||
Constructors require `new`
|
||||
: Unlike a regular function, a class `constructor` can't be called without `new`:
|
||||
|
||||
```js run
|
||||
class User {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
alert(typeof User); // function
|
||||
User(); // Error: Class constructor User cannot be invoked without 'new'
|
||||
```
|
||||
|
||||
Different string output
|
||||
: If we output it like `alert(User)`, some engines show `"class User..."`, while others show `"function User..."`.
|
||||
|
||||
Please don't be confused: the string representation may vary, but that's still a function, there is no separate "class" entity in JavaScript language.
|
||||
|
||||
Class methods are non-enumerable
|
||||
: A class definition sets `enumerable` flag to `false` for all methods in the `"prototype"`. That's good, because if we `for..in` over an object, we usually don't want its class methods.
|
||||
|
||||
Classes have a default `constructor() {}`
|
||||
: If there's no `constructor` in the `class` construct, then an empty function is generated, same as if we had written `constructor() {}`.
|
||||
|
||||
Classes always `use strict`
|
||||
: All code inside the class construct is automatically in strict mode.
|
||||
|
||||
|
||||
## Getters/setters, other shorthands
|
||||
|
||||
Classes also include getters/setters, generators, computed properties etc.
|
||||
|
||||
Here's an example for `user.name` implemented using `get/set`:
|
||||
|
||||
```js run
|
||||
class User {
|
||||
|
||||
constructor(name) {
|
||||
// invokes the setter
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
*!*
|
||||
get name() {
|
||||
*/!*
|
||||
return this._name;
|
||||
}
|
||||
|
||||
*!*
|
||||
set name(value) {
|
||||
*/!*
|
||||
if (value.length < 4) {
|
||||
alert("Name is too short.");
|
||||
return;
|
||||
}
|
||||
this._name = value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let user = new User("John");
|
||||
alert(user.name); // John
|
||||
|
||||
user = new User(""); // Name too short.
|
||||
```
|
||||
|
||||
Internally, getters and setters are created on `User.prototype`, like this:
|
||||
|
||||
```js
|
||||
Object.defineProperties(User.prototype, {
|
||||
name: {
|
||||
get() {
|
||||
return this._name
|
||||
},
|
||||
set(name) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Here's an example with computed properties:
|
||||
|
||||
```js run
|
||||
function f() { return "sayHi"; }
|
||||
|
||||
class User {
|
||||
[f()]() {
|
||||
alert("Hello");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
new User().sayHi();
|
||||
```
|
||||
|
||||
For a generator method, similarly, prepend it with `*`.
|
||||
|
||||
## Class properties
|
||||
|
||||
```warn header="Old browsers may need a polyfill"
|
||||
Class-level properties is a recent addition to the language.
|
||||
```
|
||||
|
||||
In the example before, `User` only had methods. Let's add a property:
|
||||
|
||||
```js run
|
||||
class User {
|
||||
name = "Anonymous";
|
||||
|
||||
sayHi() {
|
||||
alert(`Hello, ${this.name}!`);
|
||||
}
|
||||
}
|
||||
|
||||
new User().sayHi();
|
||||
```
|
||||
|
||||
The property is not placed into `User.prototype`. Instead, it is created by `new`, separately for every object. So, the property will never be shared between different objects of the same class.
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
The basic class syntax looks like this:
|
||||
|
||||
```js
|
||||
class MyClass {
|
||||
prop = value;
|
||||
|
||||
constructor(...) {
|
||||
// ...
|
||||
}
|
||||
|
||||
method(...) {}
|
||||
|
||||
get something(...) {}
|
||||
set something(...) {}
|
||||
|
||||
[Symbol.iterator]() {}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
`MyClass` is technically a function, while methods are written to `MyClass.prototype`.
|
||||
|
||||
In the next chapters we'll learn more about classes, including inheritance and other features.
|
BIN
1-js/09-classes/02-class/class-user.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
1-js/09-classes/02-class/class-user@2x.png
Normal file
After Width: | Height: | Size: 28 KiB |
|
@ -0,0 +1,27 @@
|
|||
That's because the child constructor must call `super()`.
|
||||
|
||||
Here's the corrected code:
|
||||
|
||||
```js run
|
||||
class Animal {
|
||||
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Rabbit extends Animal {
|
||||
constructor(name) {
|
||||
*!*
|
||||
super(name);
|
||||
*/!*
|
||||
this.created = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
*!*
|
||||
let rabbit = new Rabbit("White Rabbit"); // ok now
|
||||
*/!*
|
||||
alert(rabbit.name); // White Rabbit
|
||||
```
|
|
@ -0,0 +1,30 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Error creating an instance
|
||||
|
||||
Here's the code with `Rabbit` extending `Animal`.
|
||||
|
||||
Unfortunately, `Rabbit` objects can't be created. What's wrong? Fix it.
|
||||
```js run
|
||||
class Animal {
|
||||
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Rabbit extends Animal {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.created = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
*!*
|
||||
let rabbit = new Rabbit("White Rabbit"); // Error: this is not defined
|
||||
*/!*
|
||||
alert(rabbit.name);
|
||||
```
|
|
@ -0,0 +1,34 @@
|
|||
class Clock {
|
||||
constructor({ template }) {
|
||||
this._template = template;
|
||||
}
|
||||
|
||||
_render() {
|
||||
let date = new Date();
|
||||
|
||||
let hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
let mins = date.getMinutes();
|
||||
if (mins < 10) mins = '0' + mins;
|
||||
|
||||
let secs = date.getSeconds();
|
||||
if (secs < 10) secs = '0' + secs;
|
||||
|
||||
let output = this._template
|
||||
.replace('h', hours)
|
||||
.replace('m', mins)
|
||||
.replace('s', secs);
|
||||
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this._timer);
|
||||
}
|
||||
|
||||
start() {
|
||||
this._render();
|
||||
this._timer = setInterval(() => this._render(), 1000);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
class ExtendedClock extends Clock {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
let { precision=1000 } = options;
|
||||
this._precision = precision;
|
||||
}
|
||||
|
||||
start() {
|
||||
this._render();
|
||||
this._timer = setInterval(() => this._render(), this._precision);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Console clock</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="clock.js"></script>
|
||||
<script src="extended-clock.js"></script>
|
||||
|
||||
<script>
|
||||
let lowResolutionClock = new ExtendedClock({
|
||||
template: 'h:m:s',
|
||||
precision: 10000
|
||||
});
|
||||
|
||||
lowResolutionClock.start();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,34 @@
|
|||
class Clock {
|
||||
constructor({ template }) {
|
||||
this._template = template;
|
||||
}
|
||||
|
||||
_render() {
|
||||
let date = new Date();
|
||||
|
||||
let hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
let mins = date.getMinutes();
|
||||
if (mins < 10) mins = '0' + mins;
|
||||
|
||||
let secs = date.getSeconds();
|
||||
if (secs < 10) secs = '0' + secs;
|
||||
|
||||
let output = this._template
|
||||
.replace('h', hours)
|
||||
.replace('m', mins)
|
||||
.replace('s', secs);
|
||||
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this._timer);
|
||||
}
|
||||
|
||||
start() {
|
||||
this._render();
|
||||
this._timer = setInterval(() => this._render(), 1000);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Console clock</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- source clock -->
|
||||
<script src="clock.js"></script>
|
||||
<script>
|
||||
let clock = new Clock({
|
||||
template: 'h:m:s'
|
||||
});
|
||||
clock.start();
|
||||
|
||||
|
||||
/* Your class should work like this: */
|
||||
|
||||
/*
|
||||
|
||||
let lowResolutionClock = new ExtendedClock({
|
||||
template: 'h:m:s',
|
||||
precision: 10000
|
||||
});
|
||||
|
||||
lowResolutionClock.start();
|
||||
*/
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,12 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Extended clock
|
||||
|
||||
We've got a `Clock` class. As of now, it prints the time every second.
|
||||
|
||||
Create a new class `ExtendedClock` that inherits from `Clock` and adds the parameter `precision` -- the number of `ms` between "ticks". Should be `1000` (1 second) by default.
|
||||
|
||||
- Your code should be in the file `extended-clock.js`
|
||||
- Don't modify the original `clock.js`. Extend it.
|
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 62 KiB |
|
@ -0,0 +1,81 @@
|
|||
First, let's see why the latter code doesn't work.
|
||||
|
||||
The reason becomes obvious if we try to run it. An inheriting class constructor must call `super()`. Otherwise `"this"` won't be "defined".
|
||||
|
||||
So here's the fix:
|
||||
|
||||
```js run
|
||||
class Rabbit extends Object {
|
||||
constructor(name) {
|
||||
*!*
|
||||
super(); // need to call the parent constructor when inheriting
|
||||
*/!*
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
let rabbit = new Rabbit("Rab");
|
||||
|
||||
alert( rabbit.hasOwnProperty('name') ); // true
|
||||
```
|
||||
|
||||
But that's not all yet.
|
||||
|
||||
Even after the fix, there's still important difference in `"class Rabbit extends Object"` versus `class Rabbit`.
|
||||
|
||||
As we know, the "extends" syntax sets up two prototypes:
|
||||
|
||||
1. Between `"prototype"` of the constructor functions (for methods).
|
||||
2. Between the constructor functions itself (for static methods).
|
||||
|
||||
In our case, for `class Rabbit extends Object` it means:
|
||||
|
||||
```js run
|
||||
class Rabbit extends Object {}
|
||||
|
||||
alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
|
||||
alert( Rabbit.__proto__ === Object ); // (2) true
|
||||
```
|
||||
|
||||
So `Rabbit` now provides access to static methods of `Object` via `Rabbit`, like this:
|
||||
|
||||
```js run
|
||||
class Rabbit extends Object {}
|
||||
|
||||
*!*
|
||||
// normally we call Object.getOwnPropertyNames
|
||||
alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // a,b
|
||||
*/!*
|
||||
```
|
||||
|
||||
But if we don't have `extends Object`, then `Rabbit.__proto__` is not set to `Object`.
|
||||
|
||||
Here's the demo:
|
||||
|
||||
```js run
|
||||
class Rabbit {}
|
||||
|
||||
alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
|
||||
alert( Rabbit.__proto__ === Object ); // (2) false (!)
|
||||
alert( Rabbit.__proto__ === Function.prototype ); // as any function by default
|
||||
|
||||
*!*
|
||||
// error, no such function in Rabbit
|
||||
alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // Error
|
||||
*/!*
|
||||
```
|
||||
|
||||
So `Rabbit` doesn't provide access to static methods of `Object` in that case.
|
||||
|
||||
By the way, `Function.prototype` has "generic" function methods, like `call`, `bind` etc. They are ultimately available in both cases, because for the built-in `Object` constructor, `Object.__proto__ === Function.prototype`.
|
||||
|
||||
Here's the picture:
|
||||
|
||||

|
||||
|
||||
So, to put it short, there are two differences:
|
||||
|
||||
| class Rabbit | class Rabbit extends Object |
|
||||
|--------------|------------------------------|
|
||||
| -- | needs to call `super()` in constructor |
|
||||
| `Rabbit.__proto__ === Function.prototype` | `Rabbit.__proto__ === Object` |
|
|
@ -0,0 +1,43 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Class extends Object?
|
||||
|
||||
As we know, all objects normally inherit from `Object.prototype` and get access to "generic" object methods like `hasOwnProperty` etc.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
class Rabbit {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
let rabbit = new Rabbit("Rab");
|
||||
|
||||
*!*
|
||||
// hasOwnProperty method is from Object.prototype
|
||||
// rabbit.__proto__ === Object.prototype
|
||||
alert( rabbit.hasOwnProperty('name') ); // true
|
||||
*/!*
|
||||
```
|
||||
|
||||
But if we spell it out explicitly like `"class Rabbit extends Object"`, then the result would be different from a simple `"class Rabbit"`?
|
||||
|
||||
What's the difference?
|
||||
|
||||
Here's an example of such code (it doesn't work -- why? fix it?):
|
||||
|
||||
```js
|
||||
class Rabbit extends Object {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
let rabbit = new Rabbit("Rab");
|
||||
|
||||
alert( rabbit.hasOwnProperty('name') ); // true
|
||||
```
|
BIN
1-js/09-classes/03-class-inheritance/animal-rabbit-extends.png
Normal file
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 72 KiB |
431
1-js/09-classes/03-class-inheritance/article.md
Normal file
|
@ -0,0 +1,431 @@
|
|||
|
||||
# Class inheritance
|
||||
|
||||
Classes can extend one another. There's a nice syntax, technically based on the prototypal inheritance.
|
||||
|
||||
To inherit from another class, we should specify `"extends"` and the parent class before the brackets `{..}`.
|
||||
|
||||
Here `Rabbit` inherits from `Animal`:
|
||||
|
||||
```js run
|
||||
class Animal {
|
||||
|
||||
constructor(name) {
|
||||
this.speed = 0;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
run(speed) {
|
||||
this.speed += speed;
|
||||
alert(`${this.name} runs with speed ${this.speed}.`);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.speed = 0;
|
||||
alert(`${this.name} stopped.`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
*!*
|
||||
// Inherit from Animal
|
||||
class Rabbit extends Animal {
|
||||
hide() {
|
||||
alert(`${this.name} hides!`);
|
||||
}
|
||||
}
|
||||
*/!*
|
||||
|
||||
let rabbit = new Rabbit("White Rabbit");
|
||||
|
||||
rabbit.run(5); // White Rabbit runs with speed 5.
|
||||
rabbit.hide(); // White Rabbit hides!
|
||||
```
|
||||
|
||||
The `extends` keyword actually adds a `[[Prototype]]` reference from `Rabbit.prototype` to `Animal.prototype`, just as you expect it to be, and as we've seen before.
|
||||
|
||||

|
||||
|
||||
So now `rabbit` has access both to its own methods and to methods of `Animal`.
|
||||
|
||||
````smart header="Any expression is allowed after `extends`"
|
||||
Class syntax allows to specify not just a class, but any expression after `extends`.
|
||||
|
||||
For instance, a function call that generates the parent class:
|
||||
|
||||
```js run
|
||||
function f(phrase) {
|
||||
return class {
|
||||
sayHi() { alert(phrase) }
|
||||
}
|
||||
}
|
||||
|
||||
*!*
|
||||
class User extends f("Hello") {}
|
||||
*/!*
|
||||
|
||||
new User().sayHi(); // Hello
|
||||
```
|
||||
Here `class User` inherits from the result of `f("Hello")`.
|
||||
|
||||
That may be useful for advanced programming patterns when we use functions to generate classes depending on many conditions and can inherit from them.
|
||||
````
|
||||
|
||||
## Overriding a method
|
||||
|
||||
Now let's move forward and override a method. As of now, `Rabbit` inherits the `stop` method that sets `this.speed = 0` from `Animal`.
|
||||
|
||||
If we specify our own `stop` in `Rabbit`, then it will be used instead:
|
||||
|
||||
```js
|
||||
class Rabbit extends Animal {
|
||||
stop() {
|
||||
// ...this will be used for rabbit.stop()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
...But usually we don't want to totally replace a parent method, but rather to build on top of it, tweak or extend its functionality. We do something in our method, but call the parent method before/after it or in the process.
|
||||
|
||||
Classes provide `"super"` keyword for that.
|
||||
|
||||
- `super.method(...)` to call a parent method.
|
||||
- `super(...)` to call a parent constructor (inside our constructor only).
|
||||
|
||||
For instance, let our rabbit autohide when stopped:
|
||||
|
||||
```js run
|
||||
class Animal {
|
||||
|
||||
constructor(name) {
|
||||
this.speed = 0;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
run(speed) {
|
||||
this.speed += speed;
|
||||
alert(`${this.name} runs with speed ${this.speed}.`);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.speed = 0;
|
||||
alert(`${this.name} stopped.`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Rabbit extends Animal {
|
||||
hide() {
|
||||
alert(`${this.name} hides!`);
|
||||
}
|
||||
|
||||
*!*
|
||||
stop() {
|
||||
super.stop(); // call parent stop
|
||||
this.hide(); // and then hide
|
||||
}
|
||||
*/!*
|
||||
}
|
||||
|
||||
let rabbit = new Rabbit("White Rabbit");
|
||||
|
||||
rabbit.run(5); // White Rabbit runs with speed 5.
|
||||
rabbit.stop(); // White Rabbit stopped. White rabbit hides!
|
||||
```
|
||||
|
||||
Now `Rabbit` has the `stop` method that calls the parent `super.stop()` in the process.
|
||||
|
||||
````smart header="Arrow functions have no `super`"
|
||||
As was mentioned in the chapter <info:arrow-functions>, arrow functions do not have `super`.
|
||||
|
||||
If accessed, it's taken from the outer function. For instance:
|
||||
```js
|
||||
class Rabbit extends Animal {
|
||||
stop() {
|
||||
setTimeout(() => super.stop(), 1000); // call parent stop after 1sec
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `super` in the arrow function is the same as in `stop()`, so it works as intended. If we specified a "regular" function here, there would be an error:
|
||||
|
||||
```js
|
||||
// Unexpected super
|
||||
setTimeout(function() { super.stop() }, 1000);
|
||||
```
|
||||
````
|
||||
|
||||
|
||||
## Overriding constructor
|
||||
|
||||
With constructors it gets a little bit tricky.
|
||||
|
||||
Till now, `Rabbit` did not have its own `constructor`.
|
||||
|
||||
According to the [specification](https://tc39.github.io/ecma262/#sec-runtime-semantics-classdefinitionevaluation), if a class extends another class and has no `constructor`, then the following `constructor` is generated:
|
||||
|
||||
```js
|
||||
class Rabbit extends Animal {
|
||||
// generated for extending classes without own constructors
|
||||
*!*
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
*/!*
|
||||
}
|
||||
```
|
||||
|
||||
As we can see, it basically calls the parent `constructor` passing it all the arguments. That happens if we don't write a constructor of our own.
|
||||
|
||||
Now let's add a custom constructor to `Rabbit`. It will specify the `earLength` in addition to `name`:
|
||||
|
||||
```js run
|
||||
class Animal {
|
||||
constructor(name) {
|
||||
this.speed = 0;
|
||||
this.name = name;
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
class Rabbit extends Animal {
|
||||
|
||||
*!*
|
||||
constructor(name, earLength) {
|
||||
this.speed = 0;
|
||||
this.name = name;
|
||||
this.earLength = earLength;
|
||||
}
|
||||
*/!*
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
*!*
|
||||
// Doesn't work!
|
||||
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.
|
||||
*/!*
|
||||
```
|
||||
|
||||
Whoops! We've got an error. Now we can't create rabbits. What went wrong?
|
||||
|
||||
The short answer is: constructors in inheriting classes must call `super(...)`, and (!) do it before using `this`.
|
||||
|
||||
...But why? What's going on here? Indeed, the requirement seems strange.
|
||||
|
||||
Of course, there's an explanation. Let's get into details, so you'd really understand what's going on.
|
||||
|
||||
In JavaScript, there's a distinction between a "constructor function of an inheriting class" and all others. In an inheriting class, the corresponding constructor function is labelled with a special internal property `[[ConstructorKind]]:"derived"`.
|
||||
|
||||
The difference is:
|
||||
|
||||
- When a normal constructor runs, it creates an empty object as `this` and continues with it.
|
||||
- But when a derived constructor runs, it doesn't do it. It expects the parent constructor to do this job.
|
||||
|
||||
So if we're making a constructor of our own, then we must call `super`, because otherwise the object with `this` reference to it won't be created. And we'll get an error.
|
||||
|
||||
For `Rabbit` to work, we need to call `super()` before using `this`, like here:
|
||||
|
||||
```js run
|
||||
class Animal {
|
||||
|
||||
constructor(name) {
|
||||
this.speed = 0;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
class Rabbit extends Animal {
|
||||
|
||||
constructor(name, earLength) {
|
||||
*!*
|
||||
super(name);
|
||||
*/!*
|
||||
this.earLength = earLength;
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
*!*
|
||||
// now fine
|
||||
let rabbit = new Rabbit("White Rabbit", 10);
|
||||
alert(rabbit.name); // White Rabbit
|
||||
alert(rabbit.earLength); // 10
|
||||
*/!*
|
||||
```
|
||||
|
||||
|
||||
## Super: internals, [[HomeObject]]
|
||||
|
||||
Let's get a little deeper under the hood of `super`. We'll see some interesting things by the way.
|
||||
|
||||
First to say, from all that we've learned till now, it's impossible for `super` to work.
|
||||
|
||||
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, how to retrieve the `method`? Naturally, we need to take the `method` from the prototype of the current object. How, technically, we (or a JavaScript engine) can do it?
|
||||
|
||||
Maybe we can get the method from `[[Prototype]]` of `this`, as `this.__proto__.method`? Unfortunately, that doesn't work.
|
||||
|
||||
Let's try to do it. Without classes, using plain objects for the sake of simplicity.
|
||||
|
||||
Here, `rabbit.eat()` should call `animal.eat()` method of the parent object:
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
name: "Animal",
|
||||
eat() {
|
||||
alert(`${this.name} eats.`);
|
||||
}
|
||||
};
|
||||
|
||||
let rabbit = {
|
||||
__proto__: animal,
|
||||
name: "Rabbit",
|
||||
eat() {
|
||||
*!*
|
||||
// that's how super.eat() could presumably work
|
||||
this.__proto__.eat.call(this); // (*)
|
||||
*/!*
|
||||
}
|
||||
};
|
||||
|
||||
rabbit.eat(); // Rabbit eats.
|
||||
```
|
||||
|
||||
At the line `(*)` we take `eat` from the prototype (`animal`) and call it in the context of the current object. Please note that `.call(this)` is important here, because a simple `this.__proto__.eat()` would execute parent `eat` in the context of the prototype, not the current object.
|
||||
|
||||
And in the code above it actually works as intended: we have the correct `alert`.
|
||||
|
||||
Now let's add one more object to the chain. We'll see how things break:
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
name: "Animal",
|
||||
eat() {
|
||||
alert(`${this.name} eats.`);
|
||||
}
|
||||
};
|
||||
|
||||
let rabbit = {
|
||||
__proto__: animal,
|
||||
eat() {
|
||||
// ...bounce around rabbit-style and call parent (animal) method
|
||||
this.__proto__.eat.call(this); // (*)
|
||||
}
|
||||
};
|
||||
|
||||
let longEar = {
|
||||
__proto__: rabbit,
|
||||
eat() {
|
||||
// ...do something with long ears and call parent (rabbit) method
|
||||
this.__proto__.eat.call(this); // (**)
|
||||
}
|
||||
};
|
||||
|
||||
*!*
|
||||
longEar.eat(); // Error: Maximum call stack size exceeded
|
||||
*/!*
|
||||
```
|
||||
|
||||
The code doesn't work anymore! We can see the error trying to call `longEar.eat()`.
|
||||
|
||||
It may be not that obvious, but if we trace `longEar.eat()` call, then we can see why. In both lines `(*)` and `(**)` the value of `this` is the current object (`longEar`). That's essential: all object methods get the current object as `this`, not a prototype or something.
|
||||
|
||||
So, in both lines `(*)` and `(**)` the value of `this.__proto__` is exactly the same: `rabbit`. They both call `rabbit.eat` without going up the chain in the endless loop.
|
||||
|
||||
Here's the picture of what happens:
|
||||
|
||||

|
||||
|
||||
1. Inside `longEar.eat()`, the line `(**)` calls `rabbit.eat` providing it with `this=longEar`.
|
||||
```js
|
||||
// inside longEar.eat() we have this = longEar
|
||||
this.__proto__.eat.call(this) // (**)
|
||||
// becomes
|
||||
longEar.__proto__.eat.call(this)
|
||||
// that is
|
||||
rabbit.eat.call(this);
|
||||
```
|
||||
2. Then in the line `(*)` of `rabbit.eat`, we'd like to pass the call even higher in the chain, but `this=longEar`, so `this.__proto__.eat` is again `rabbit.eat`!
|
||||
|
||||
```js
|
||||
// inside rabbit.eat() we also have this = longEar
|
||||
this.__proto__.eat.call(this) // (*)
|
||||
// becomes
|
||||
longEar.__proto__.eat.call(this)
|
||||
// or (again)
|
||||
rabbit.eat.call(this);
|
||||
```
|
||||
|
||||
3. ...So `rabbit.eat` calls itself in the endless loop, because it can't ascend any further.
|
||||
|
||||
The problem can't be solved by using `this` alone.
|
||||
|
||||
### `[[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.**
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Let's see how it works for `super` -- again, using plain objects:
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
name: "Animal",
|
||||
eat() { // [[HomeObject]] == animal
|
||||
alert(`${this.name} eats.`);
|
||||
}
|
||||
};
|
||||
|
||||
let rabbit = {
|
||||
__proto__: animal,
|
||||
name: "Rabbit",
|
||||
eat() { // [[HomeObject]] == rabbit
|
||||
super.eat();
|
||||
}
|
||||
};
|
||||
|
||||
let longEar = {
|
||||
__proto__: rabbit,
|
||||
name: "Long Ear",
|
||||
eat() { // [[HomeObject]] == longEar
|
||||
super.eat();
|
||||
}
|
||||
};
|
||||
|
||||
*!*
|
||||
longEar.eat(); // Long Ear eats.
|
||||
*/!*
|
||||
```
|
||||
|
||||
Every method remembers its object in the internal `[[HomeObject]]` property. Then `super` uses it to resolve the parent prototype.
|
||||
|
||||
`[[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()"`.
|
||||
|
||||
In the example below a non-method syntax is used for comparison. `[[HomeObject]]` property is not set and the inheritance doesn't work:
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
eat: function() { // should be the short syntax: eat() {...}
|
||||
// ...
|
||||
}
|
||||
};
|
||||
|
||||
let rabbit = {
|
||||
__proto__: animal,
|
||||
eat: function() {
|
||||
super.eat();
|
||||
}
|
||||
};
|
||||
|
||||
*!*
|
||||
rabbit.eat(); // Error calling super (because there's no [[HomeObject]])
|
||||
*/!*
|
||||
```
|
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 44 KiB |
BIN
1-js/09-classes/03-class-inheritance/this-super-loop.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
1-js/09-classes/03-class-inheritance/this-super-loop@2x.png
Normal file
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 70 KiB |
226
1-js/09-classes/04-static-properties-methods/article.md
Normal file
|
@ -0,0 +1,226 @@
|
|||
|
||||
# Static properties and methods
|
||||
|
||||
We can also assign a methods to the class function, not to its `"prototype"`. Such methods are called *static*.
|
||||
|
||||
An example:
|
||||
|
||||
```js run
|
||||
class User {
|
||||
*!*
|
||||
static staticMethod() {
|
||||
*/!*
|
||||
alert(this === User);
|
||||
}
|
||||
}
|
||||
|
||||
User.staticMethod(); // true
|
||||
```
|
||||
|
||||
That actually does the same as assigning it as a function property:
|
||||
|
||||
```js
|
||||
function User() { }
|
||||
|
||||
User.staticMethod = function() {
|
||||
alert(this === User);
|
||||
};
|
||||
```
|
||||
|
||||
The value of `this` inside `User.staticMethod()` is the class constructor `User` itself (the "object before dot" rule).
|
||||
|
||||
Usually, static methods are used to implement functions that belong to the class, but not to any particular object of it.
|
||||
|
||||
For instance, we have `Article` objects and need a function to compare them. The natural choice would be `Article.compare`, like this:
|
||||
|
||||
```js run
|
||||
class Article {
|
||||
constructor(title, date) {
|
||||
this.title = title;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
*!*
|
||||
static compare(articleA, articleB) {
|
||||
return articleA.date - articleB.date;
|
||||
}
|
||||
*/!*
|
||||
}
|
||||
|
||||
// usage
|
||||
let articles = [
|
||||
new Article("Mind", new Date(2019, 1, 1)),
|
||||
new Article("Body", new Date(2019, 0, 1)),
|
||||
new Article("JavaScript", new Date(2019, 11, 1))
|
||||
];
|
||||
|
||||
*!*
|
||||
articles.sort(Article.compare);
|
||||
*/!*
|
||||
|
||||
alert( articles[0].title ); // Body
|
||||
```
|
||||
|
||||
Here `Article.compare` stands "over" the articles, as a means to compare them. It's not a method of an article, but rather of the whole class.
|
||||
|
||||
Another example would be a so-called "factory" method. Imagine, we need few ways to create an article:
|
||||
|
||||
1. Create by given parameters (`title`, `date` etc).
|
||||
2. Create an empty article with today's date.
|
||||
3. ...
|
||||
|
||||
The first way can be implemented by the constructor. And for the second one we can make a static method of the class.
|
||||
|
||||
Like `Article.createTodays()` here:
|
||||
|
||||
```js run
|
||||
class Article {
|
||||
constructor(title, date) {
|
||||
this.title = title;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
*!*
|
||||
static createTodays() {
|
||||
// remember, this = Article
|
||||
return new this("Today's digest", new Date());
|
||||
}
|
||||
*/!*
|
||||
}
|
||||
|
||||
let article = Article.createTodays();
|
||||
|
||||
alert( article.title ); // Todays digest
|
||||
```
|
||||
|
||||
Now every time we need to create a today's digest, we can call `Article.createTodays()`. Once again, that's not a method of an article, but a method of the whole class.
|
||||
|
||||
Static methods are also used in database-related classes to search/save/remove entries from the database, like this:
|
||||
|
||||
```js
|
||||
// assuming Article is a special class for managing articles
|
||||
// static method to remove the article:
|
||||
Article.remove({id: 12345});
|
||||
```
|
||||
|
||||
## Static properties
|
||||
|
||||
[recent browser=Chrome]
|
||||
|
||||
Static properties are also possible, just like regular class properties:
|
||||
|
||||
```js run
|
||||
class Article {
|
||||
static publisher = "Ilya Kantor";
|
||||
}
|
||||
|
||||
alert( Article.publisher ); // Ilya Kantor
|
||||
```
|
||||
|
||||
That is the same as a direct assignment to `Article`:
|
||||
|
||||
```js
|
||||
Article.publisher = "Ilya Kantor";
|
||||
```
|
||||
|
||||
## Statics and inheritance
|
||||
|
||||
Statics are inhereted, we can access `Parent.method` as `Child.method`.
|
||||
|
||||
For instance, `Animal.compare` in the code below is inhereted and accessible as `Rabbit.compare`:
|
||||
|
||||
```js run
|
||||
class Animal {
|
||||
|
||||
constructor(name, speed) {
|
||||
this.speed = speed;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
run(speed = 0) {
|
||||
this.speed += speed;
|
||||
alert(`${this.name} runs with speed ${this.speed}.`);
|
||||
}
|
||||
|
||||
*!*
|
||||
static compare(animalA, animalB) {
|
||||
return animalA.speed - animalB.speed;
|
||||
}
|
||||
*/!*
|
||||
|
||||
}
|
||||
|
||||
// Inherit from Animal
|
||||
class Rabbit extends Animal {
|
||||
hide() {
|
||||
alert(`${this.name} hides!`);
|
||||
}
|
||||
}
|
||||
|
||||
let rabbits = [
|
||||
new Rabbit("White Rabbit", 10),
|
||||
new Rabbit("Black Rabbit", 5)
|
||||
];
|
||||
|
||||
*!*
|
||||
rabbits.sort(Rabbit.compare);
|
||||
*/!*
|
||||
|
||||
rabbits[0].run(); // Black Rabbit runs with speed 5.
|
||||
```
|
||||
|
||||
Now we can call `Rabbit.compare` assuming that the inherited `Animal.compare` will be called.
|
||||
|
||||
How does it work? Again, using prototypes. As you might have already guessed, extends also gives `Rabbit` the `[[Prototype]]` reference to `Animal`.
|
||||
|
||||
|
||||

|
||||
|
||||
So, `Rabbit` function now inherits from `Animal` function. And `Animal` function normally has `[[Prototype]]` referencing `Function.prototype`, because it doesn't `extend` anything.
|
||||
|
||||
Here, let's check that:
|
||||
|
||||
```js run
|
||||
class Animal {}
|
||||
class Rabbit extends Animal {}
|
||||
|
||||
// for static properties and methods
|
||||
alert(Rabbit.__proto__ === Animal); // true
|
||||
|
||||
// and the next step is Function.prototype
|
||||
alert(Animal.__proto__ === Function.prototype); // true
|
||||
|
||||
// that's in addition to the "normal" prototype chain for object methods
|
||||
alert(Rabbit.prototype.__proto__ === Animal.prototype);
|
||||
```
|
||||
|
||||
This way `Rabbit` has access to all static methods of `Animal`.
|
||||
|
||||
## Summary
|
||||
|
||||
Static methods are used for the functionality that doesn't relate to a concrete class instance, doesn't require an instance to exist, but rather belongs to the class as a whole, like `Article.compare` -- a generic method to compare two articles.
|
||||
|
||||
Static properties are used when we'd like to store class-level data, also not bound to an instance.
|
||||
|
||||
The syntax is:
|
||||
|
||||
```js
|
||||
class MyClass {
|
||||
static property = ...;
|
||||
|
||||
static method() {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That's technically the same as assigning to the class itself:
|
||||
|
||||
```js
|
||||
MyClass.property = ...
|
||||
MyClass.method = ...
|
||||
```
|
||||
|
||||
Static properties are inherited.
|
||||
|
||||
Technically, 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`.
|
|
@ -0,0 +1,330 @@
|
|||
|
||||
# Private and protected properties and methods
|
||||
|
||||
One of the most important principles of object oriented programming -- delimiting internal interface from the external one.
|
||||
|
||||
That is "a must" practice in developing anything more complex than a "hello world" app.
|
||||
|
||||
To understand this, let's break away from development and turn our eyes into the real world.
|
||||
|
||||
Usually, devices that we're using are quite complex. But delimiting the internal interface from the external one allows to use them without problems.
|
||||
|
||||
## A real-life example
|
||||
|
||||
For instance, a coffee machine. Simple from outside: a button, a display, a few holes...And, surely, the result -- great coffee! :)
|
||||
|
||||

|
||||
|
||||
But inside... (a picture from the repair manual)
|
||||
|
||||

|
||||
|
||||
A lot of details. But we can use it without knowing anything.
|
||||
|
||||
Coffee machines are quite reliable, aren't they? We can use one for years, and only if something goes wrong -- bring it for repairs.
|
||||
|
||||
The secret of reliability and simplicity of a coffee machine -- all details are well-tuned and *hidden* inside.
|
||||
|
||||
If we remove the protective cover from the coffee machine, then using it will be much more complex (where to press?), and dangerous (it can electrocute).
|
||||
|
||||
As we'll see, in programming objects are like coffee machines.
|
||||
|
||||
But in order to hide inner details, we'll use not a protective cover, but rather special syntax of the language and conventions.
|
||||
|
||||
## Internal and external interface
|
||||
|
||||
In object-oriented programming, properties and methods are split into two groups:
|
||||
|
||||
- *Internal interface* -- methods and properties, accessible from other methods of the class, but not from the outside.
|
||||
- *External interface* -- methods and properties, accessible also from outside the class.
|
||||
|
||||
If we continue the analogy with the coffee machine -- what's hidden inside: a boiler tube, heating element, and so on -- is its internal interface.
|
||||
|
||||
An internal interface is used for the object to work, its details use each other. For instance, a boiler tube is attached to the heating element.
|
||||
|
||||
But from the outside a coffee machine is closed by the protective cover, so that no one can reach those. Details are hidden and inaccessible. We can use its features via the external interface.
|
||||
|
||||
So, all we need to use an object is to know its external interface. We may be completely unaware how it works inside, and that's great.
|
||||
|
||||
That was a general introduction.
|
||||
|
||||
In JavaScript, there are three types of properties and members:
|
||||
|
||||
- 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 properly do the extension.
|
||||
|
||||
Protected fields are not implemented in Javascript on the language level, but in practice they are very convenient, so they are emulated.
|
||||
|
||||
In the next step we'll make a coffee machine in Javascript with all these types of properties. A coffee machine has a lot of details, we won't model them to stay simple (though we could).
|
||||
|
||||
## Protecting "waterAmount"
|
||||
|
||||
Let's make a simple coffee machine class first:
|
||||
|
||||
```js run
|
||||
class CoffeeMachine {
|
||||
waterAmount = 0; // the amount of water inside
|
||||
|
||||
constructor(power) {
|
||||
this.power = power;
|
||||
alert( `Created a coffee-machine, power: ${power}` );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// create the coffee machine
|
||||
let coffeeMachine = new CoffeeMachine(100);
|
||||
|
||||
// add water
|
||||
coffeeMachine.waterAmount = 200;
|
||||
```
|
||||
|
||||
Right now the properties `waterAmount` and `power` are public. We can easily get/set them from the outside to any value.
|
||||
|
||||
Let's change `waterAmount` property to protected to have more control over it. For instance, we don't want anyone to set it below zero.
|
||||
|
||||
**Protected properties are usually prefixed with an underscore `_`.**
|
||||
|
||||
That is not enforced on the language level, but there's a convention that such properties and methods should not be accessed from the outside. Most programmers follow it.
|
||||
|
||||
So our property will be called `_waterAmount`:
|
||||
|
||||
```js run
|
||||
class CoffeeMachine {
|
||||
_waterAmount = 0;
|
||||
|
||||
set waterAmount(value) {
|
||||
if (value < 0) throw new Error("Negative water");
|
||||
this._waterAmount = value;
|
||||
}
|
||||
|
||||
get waterAmount() {
|
||||
return this.waterAmount;
|
||||
}
|
||||
|
||||
constructor(power) {
|
||||
this._power = power;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// create the coffee machine
|
||||
let coffeeMachine = new CoffeeMachine(100);
|
||||
|
||||
// add water
|
||||
coffeeMachine.waterAmount = -10; // Error: Negative water
|
||||
```
|
||||
|
||||
Now the access is under control, so setting the water below zero fails.
|
||||
|
||||
## Read-only "power"
|
||||
|
||||
For `power` property, let's make it read-only. It sometimes happens that a property must be set at creation time only, and then never modified.
|
||||
|
||||
That's exactly the case for a coffee machine: power never changes.
|
||||
|
||||
To do so, we only need to make getter, but not the setter:
|
||||
|
||||
```js run
|
||||
class CoffeeMachine {
|
||||
// ...
|
||||
|
||||
constructor(power) {
|
||||
this._power = power;
|
||||
}
|
||||
|
||||
get power() {
|
||||
return this._power;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// create the coffee machine
|
||||
let coffeeMachine = new CoffeeMachine(100);
|
||||
|
||||
alert(`Power is: ${coffeeMachine.power}W`); // Power is: 100W
|
||||
|
||||
coffeeMachine.power = 25; // Error (no setter)
|
||||
```
|
||||
|
||||
````smart header="Getter/setter functions"
|
||||
Here we used getter/setter syntax.
|
||||
|
||||
But most of the time `get.../set...` functions are preferred, like this:
|
||||
|
||||
```js
|
||||
class CoffeeMachine {
|
||||
_waterAmount = 0;
|
||||
|
||||
*!*setWaterAmount(value)*/!* {
|
||||
if (value < 0) throw new Error("Negative water");
|
||||
this._waterAmount = value;
|
||||
}
|
||||
|
||||
*!*getWaterAmount()*/!* {
|
||||
return this.waterAmount;
|
||||
}
|
||||
}
|
||||
|
||||
new CoffeeMachine().setWaterAmount(100);
|
||||
```
|
||||
|
||||
That looks a bit longer, but functions are more flexible. They can accept multiple arguments (even if we don't need them right now). So, for the future, just in case we need to refactor something, functions are a safer choise.
|
||||
|
||||
Surely, there's a tradeoff. On the other hand, get/set syntax is shorter, so ultimately there's no strict rule, it's up to you to decide.
|
||||
````
|
||||
|
||||
```smart header="Protected fields are inherited"
|
||||
If we inherit `class MegaMachine extends CoffeeMachine`, then nothing prevents us from accessing `this._waterAmount` or `this._power` from the methods of the new class.
|
||||
|
||||
So protected fields are naturally inheritable. Unlike private ones that we'll see below.
|
||||
```
|
||||
|
||||
## Private "#waterLimit"
|
||||
|
||||
[recent browser=none]
|
||||
|
||||
There's a finished Javascript proposal, almost in the standard, that provides language-level support for private properties and methods.
|
||||
|
||||
Privates should start with `#`. They are only accessible from inside the class.
|
||||
|
||||
For instance, here we add a private `#waterLimit` property and extract the water-checking logic into a separate method:
|
||||
|
||||
```js
|
||||
class CoffeeMachine {
|
||||
*!*
|
||||
#waterLimit = 200;
|
||||
*/!*
|
||||
|
||||
*!*
|
||||
#checkWater(water) {
|
||||
if (value < 0) throw new Error("Negative water");
|
||||
if (value > this.#waterLimit) throw new Error("Too much water");
|
||||
}
|
||||
*/!*
|
||||
|
||||
_waterAmount = 0;
|
||||
|
||||
set waterAmount(value) {
|
||||
*!*
|
||||
this.#checkWater(value);
|
||||
*/!*
|
||||
this._waterAmount = value;
|
||||
}
|
||||
|
||||
get waterAmount() {
|
||||
return this.waterAmount;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let coffeeMachine = new CoffeeMachine();
|
||||
|
||||
*!*
|
||||
coffeeMachine.#checkWater(); // Error
|
||||
coffeeMachine.#waterLimit = 1000; // Error
|
||||
*/!*
|
||||
|
||||
coffeeMachine.waterAmount = 100; // Works
|
||||
```
|
||||
|
||||
On the language level, `#` is a special sign that the field is private. We can't access it from outside or from inhereting classes.
|
||||
|
||||
Private fields do not conflict with public ones. We can have both private `#waterAmount` and public `waterAmount` fields at the same time.
|
||||
|
||||
For instance, let's make `waterAmount` an accessor for `#waterAmount`:
|
||||
|
||||
```js run
|
||||
class CoffeeMachine {
|
||||
|
||||
#waterAmount = 0;
|
||||
|
||||
get waterAmount() {
|
||||
return this.#waterAmount;
|
||||
}
|
||||
|
||||
set waterAmount(value) {
|
||||
if (value < 0) throw new Error("Negative water");
|
||||
this.#waterAmount = value;
|
||||
}
|
||||
}
|
||||
|
||||
let machine = new CoffeeMachine();
|
||||
|
||||
machine.waterAmount = 100;
|
||||
alert(machine.#waterAmount); // Error
|
||||
```
|
||||
|
||||
Unlike protected ones, private fields are enforced by the language itselfs. That's a good thing.
|
||||
|
||||
But if we inherit from `CoffeeMachine`, then we'll have no direct access to `#waterAmount`. We'll need to rely on `waterAmount` getter/setter:
|
||||
|
||||
```js
|
||||
class CoffeeMachine extends CoffeeMachine() {
|
||||
method() {
|
||||
*!*
|
||||
alert( this.#waterAmount ); // Error: can only access from CoffeeMachine
|
||||
*/!*
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In many scenarios such limitation is too severe. If we extend a `CoffeeMachine`, we may have legitimate reason to access its internals. That's why protected fields are used most of the time, even though they are not supported by the language syntax.
|
||||
|
||||
````warn
|
||||
Private fields are special.
|
||||
|
||||
Remember, usually we can access fields by this[name]:
|
||||
|
||||
```js
|
||||
class User {
|
||||
...
|
||||
sayHi() {
|
||||
let fieldName = "name";
|
||||
alert(`Hello, ${this[fieldName]}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With private fields that's impossible: `this['#name']` doesn't work. That's a syntax limitation to ensure privacy.
|
||||
````
|
||||
|
||||
## Summary
|
||||
|
||||
In terms of OOP, delimiting of the internal interface from the external one is called [encapsulation]("https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)").
|
||||
|
||||
It gives the following benefits:
|
||||
|
||||
Protection for users, so that they don't shoot themselves in the feet
|
||||
: Imagine, there's a team of developers using a coffee machine. It was made by the "Best CoffeeMachine" company, and works fine, but a protective cover was removed. So the internal interface is exposed.
|
||||
|
||||
All developers are civilized -- they use the coffee machine as intended. But one of them, John, decided that he's the smartest one, and made some tweaks in the coffee machine internals. So the coffee machine failed two days later.
|
||||
|
||||
That's surely not John's fault, but rather the person who removed the protective cover and let John do his manipulations.
|
||||
|
||||
The same in programming. If a user of a class will change things not intended to be changed from the outside -- the consequences are unpredictable.
|
||||
|
||||
Supportable
|
||||
: The situation in programming is more complex than with a real-life coffee machine, because we don't just buy it once. The code constantly undergoes development and improvement.
|
||||
|
||||
**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..**
|
||||
|
||||
It's much easier to develop, if you know that certain methods can be 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, but still simple to upgrade if the external interface is the same.
|
||||
|
||||
Hiding complexity
|
||||
: People adore to use things that are simple. At least from outside. What's inside is a different thing.
|
||||
|
||||
Programmers are not an exception.
|
||||
|
||||
**It's always convenient when implementation details are hidden, and a simple, well-documented external interface is available.**
|
||||
|
||||
To hide internal interface we use either protected or public properties:
|
||||
|
||||
- Protected fields start with `_`. That's a well-known convention, not enforced at the language level. Programmers should only access a field starting with `_` from its class and classes inheriting from it.
|
||||
- Private fields start with `#`. Javascript makes sure we only can access those from inside the class.
|
||||
|
||||
Right now, private fields are not well-supported among browsers, but can be polyfilled.
|
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 12 KiB |
82
1-js/09-classes/06-extend-natives/article.md
Normal file
|
@ -0,0 +1,82 @@
|
|||
|
||||
# Extending build-in classes
|
||||
|
||||
Built-in classes like Array, Map and others are extendable also.
|
||||
|
||||
For instance, here `PowerArray` inherits from the native `Array`:
|
||||
|
||||
```js run
|
||||
// add one more method to it (can do more)
|
||||
class PowerArray extends Array {
|
||||
isEmpty() {
|
||||
return this.length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
let arr = new PowerArray(1, 2, 5, 10, 50);
|
||||
alert(arr.isEmpty()); // false
|
||||
|
||||
let filteredArr = arr.filter(item => item >= 10);
|
||||
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.
|
||||
|
||||
In the example above,
|
||||
```js
|
||||
arr.constructor === PowerArray
|
||||
```
|
||||
|
||||
So when `arr.filter()` is called, it internally creates the new array of results exactly as `new PowerArray`.
|
||||
That's actually very cool, because we can keep using `PowerArray` methods further o the result.
|
||||
|
||||
Even more, we can customize that behavior.
|
||||
|
||||
There's a special static getter `Symbol.species`, if exists, it returns the constructor to use in such cases.
|
||||
|
||||
If we'd like built-in methods like `map`, `filter` will return regular arrays, we can return `Array` in `Symbol.species`, like here:
|
||||
|
||||
```js run
|
||||
class PowerArray extends Array {
|
||||
isEmpty() {
|
||||
return this.length === 0;
|
||||
}
|
||||
|
||||
*!*
|
||||
// built-in methods will use this as the constructor
|
||||
static get [Symbol.species]() {
|
||||
return Array;
|
||||
}
|
||||
*/!*
|
||||
}
|
||||
|
||||
let arr = new PowerArray(1, 2, 5, 10, 50);
|
||||
alert(arr.isEmpty()); // false
|
||||
|
||||
// filter creates new array using arr.constructor[Symbol.species] as constructor
|
||||
let filteredArr = arr.filter(item => item >= 10);
|
||||
|
||||
*!*
|
||||
// filteredArr is not PowerArray, but Array
|
||||
*/!*
|
||||
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.
|
||||
|
||||
## No static inheritance in built-ins
|
||||
|
||||
Built-in objects have their own static methods, for instance `Object.keys`, `Array.isArray` etc.
|
||||
|
||||
And we've already been talking about native classes extending each other: `Array.[[Prototype]] = Object`.
|
||||
|
||||
But statics are an exception. Built-in classes don't inherit static properties from each other.
|
||||
|
||||
In other words, the prototype of build-in constructor `Array` does not point to `Object`. This way `Array` and `Date` do not have `Array.keys` or `Date.keys`. And that feels natural.
|
||||
|
||||
Here's the picture structure for `Date` and `Object`:
|
||||
|
||||

|
||||
|
||||
Note, there's no link between `Date` and `Object`. Both `Object` and `Date` exist independently. `Date.prototype` inherits from `Object.prototype`, but that's all.
|
BIN
1-js/09-classes/06-extend-natives/object-date-inheritance.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
1-js/09-classes/06-extend-natives/object-date-inheritance@2x.png
Normal file
After Width: | Height: | Size: 88 KiB |
|
@ -0,0 +1,7 @@
|
|||
Yeah, looks strange indeed.
|
||||
|
||||
But `instanceof` does not care about the function, but rather about its `prototype`, that it matches against the prototype chain.
|
||||
|
||||
And here `a.__proto__ == B.prototype`, so `instanceof` returns `true`.
|
||||
|
||||
So, by the logic of `instanceof`, the `prototype` actually defines the type, not the constructor function.
|
20
1-js/09-classes/07-instanceof/1-strange-instanceof/task.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Strange instanceof
|
||||
|
||||
Why `instanceof` below returns `true`? We can easily see that `a` is not created by `B()`.
|
||||
|
||||
```js run
|
||||
function A() {}
|
||||
function B() {}
|
||||
|
||||
A.prototype = B.prototype = {};
|
||||
|
||||
let a = new A();
|
||||
|
||||
*!*
|
||||
alert( a instanceof B ); // true
|
||||
*/!*
|
||||
```
|
211
1-js/09-classes/07-instanceof/article.md
Normal file
|
@ -0,0 +1,211 @@
|
|||
# Class checking: "instanceof"
|
||||
|
||||
The `instanceof` operator allows to check whether an object belongs to a certain class. It also takes inheritance into account.
|
||||
|
||||
Such a check may be necessary in many cases, here we'll use it for building a *polymorphic* function, the one that treats arguments differently depending on their type.
|
||||
|
||||
## The instanceof operator [#ref-instanceof]
|
||||
|
||||
The syntax is:
|
||||
```js
|
||||
obj instanceof Class
|
||||
```
|
||||
|
||||
It returns `true` if `obj` belongs to the `Class` (or a class inheriting from it).
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
class Rabbit {}
|
||||
let rabbit = new Rabbit();
|
||||
|
||||
// is it an object of Rabbit class?
|
||||
*!*
|
||||
alert( rabbit instanceof Rabbit ); // true
|
||||
*/!*
|
||||
```
|
||||
|
||||
It also works with constructor functions:
|
||||
|
||||
```js run
|
||||
*!*
|
||||
// instead of class
|
||||
function Rabbit() {}
|
||||
*/!*
|
||||
|
||||
alert( new Rabbit() instanceof Rabbit ); // true
|
||||
```
|
||||
|
||||
...And with built-in classes like `Array`:
|
||||
|
||||
```js run
|
||||
let arr = [1, 2, 3];
|
||||
alert( arr instanceof Array ); // true
|
||||
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, and is also fine-tunable using 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 use it. Like this:
|
||||
|
||||
```js run
|
||||
// assume anything that canEat is an animal
|
||||
class Animal {
|
||||
static [Symbol.hasInstance](obj) {
|
||||
if (obj.canEat) return true;
|
||||
}
|
||||
}
|
||||
|
||||
let obj = { canEat: true };
|
||||
alert(obj instanceof Animal); // true: Animal[Symbol.hasInstance](obj) is called
|
||||
```
|
||||
|
||||
2. Most classes do not have `Symbol.hasInstance`. In that case, check if `Class.prototype` equals to one of prototypes in the `obj` prototype chain.
|
||||
|
||||
In other words, compare:
|
||||
```js
|
||||
obj.__proto__ === Class.prototype
|
||||
obj.__proto__.__proto__ === Class.prototype
|
||||
obj.__proto__.__proto__.__proto__ === Class.prototype
|
||||
...
|
||||
```
|
||||
|
||||
In the example above `Rabbit.prototype === rabbit.__proto__`, so that gives the answer immediately.
|
||||
|
||||
In the case of an inheritance, `rabbit` is an instance of the parent class as well:
|
||||
|
||||
```js run
|
||||
class Animal {}
|
||||
class Rabbit extends Animal {}
|
||||
|
||||
let rabbit = new Rabbit();
|
||||
*!*
|
||||
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`:
|
||||
|
||||

|
||||
|
||||
By the way, there's also a method [objA.isPrototypeOf(objB)](mdn:js/object/isPrototypeOf), that returns `true` if `objA` is somewhere in the chain of prototypes for `objB`. So the test of `obj instanceof Class` can be rephrased as `Class.prototype.isPrototypeOf(obj)`.
|
||||
|
||||
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.
|
||||
|
||||
Like here:
|
||||
|
||||
```js run
|
||||
function Rabbit() {}
|
||||
let rabbit = new Rabbit();
|
||||
|
||||
// changed the prototype
|
||||
Rabbit.prototype = {};
|
||||
|
||||
// ...not a rabbit any more!
|
||||
*!*
|
||||
alert( rabbit instanceof Rabbit ); // false
|
||||
*/!*
|
||||
```
|
||||
|
||||
That's one of the reasons to avoid changing `prototype`. Just to keep safe.
|
||||
|
||||
## Bonus: Object toString for the type
|
||||
|
||||
We already know that plain objects are converted to string as `[object Object]`:
|
||||
|
||||
```js run
|
||||
let obj = {};
|
||||
|
||||
alert(obj); // [object Object]
|
||||
alert(obj.toString()); // the same
|
||||
```
|
||||
|
||||
That's their implementation of `toString`. But there's a hidden feature that makes `toString` actually much more powerful than that. We can use it as an extended `typeof` and an alternative for `instanceof`.
|
||||
|
||||
Sounds strange? Indeed. Let's demystify.
|
||||
|
||||
By [specification](https://tc39.github.io/ecma262/#sec-object.prototype.tostring), the built-in `toString` can be extracted from the object and executed in the context of any other value. And its result depends on that value.
|
||||
|
||||
- For a number, it will be `[object Number]`
|
||||
- For a boolean, it will be `[object Boolean]`
|
||||
- For `null`: `[object Null]`
|
||||
- For `undefined`: `[object Undefined]`
|
||||
- For arrays: `[object Array]`
|
||||
- ...etc (customizable).
|
||||
|
||||
Let's demonstrate:
|
||||
|
||||
```js run
|
||||
// copy toString method into a variable for convenience
|
||||
let objectToString = Object.prototype.toString;
|
||||
|
||||
// what type is this?
|
||||
let arr = [];
|
||||
|
||||
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`.
|
||||
|
||||
Internally, the `toString` algorithm examines `this` and returns the corresponding result. More examples:
|
||||
|
||||
```js run
|
||||
let s = Object.prototype.toString;
|
||||
|
||||
alert( s.call(123) ); // [object Number]
|
||||
alert( s.call(null) ); // [object Null]
|
||||
alert( s.call(alert) ); // [object Function]
|
||||
```
|
||||
|
||||
### Symbol.toStringTag
|
||||
|
||||
The behavior of Object `toString` can be customized using a special object property `Symbol.toStringTag`.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
[Symbol.toStringTag]: "User"
|
||||
};
|
||||
|
||||
alert( {}.toString.call(user) ); // [object User]
|
||||
```
|
||||
|
||||
For most environment-specific objects, there is such a property. Here are few browser specific examples:
|
||||
|
||||
```js run
|
||||
// toStringTag for the envinronment-specific object and class:
|
||||
alert( window[Symbol.toStringTag]); // window
|
||||
alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest
|
||||
|
||||
alert( {}.toString.call(window) ); // [object Window]
|
||||
alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest]
|
||||
```
|
||||
|
||||
As you can see, the result is exactly `Symbol.toStringTag` (if exists), wrapped into `[object ...]`.
|
||||
|
||||
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.
|
||||
|
||||
## Summary
|
||||
|
||||
Let's recap the type-checking methods that we know:
|
||||
|
||||
| | works for | returns |
|
||||
|---------------|-------------|---------------|
|
||||
| `typeof` | primitives | string |
|
||||
| `{}.toString` | primitives, built-in objects, objects with `Symbol.toStringTag` | string |
|
||||
| `instanceof` | objects | true/false |
|
||||
|
||||
As we can see, `{}.toString` is technically a "more advanced" `typeof`.
|
||||
|
||||
And `instanceof` operator really shines when we are working with a class hierarchy and want to check for the class taking into account inheritance.
|
BIN
1-js/09-classes/07-instanceof/instanceof.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
1-js/09-classes/07-instanceof/instanceof@2x.png
Normal file
After Width: | Height: | Size: 78 KiB |
205
1-js/09-classes/08-mixins/article.md
Normal file
|
@ -0,0 +1,205 @@
|
|||
# Mixins
|
||||
|
||||
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`.
|
||||
|
||||
Or, talking about programming, we have a class `Renderer` that implements templating and a class `EventEmitter` that implements event handling, and want to merge these functionalities together with a class `Page`, to make a page that can use templates and emit events.
|
||||
|
||||
There's a concept that can help here, called "mixins".
|
||||
|
||||
As defined in Wikipedia, a [mixin](https://en.wikipedia.org/wiki/Mixin) is a class that contains methods for use by other classes without having to be the parent class of those other classes.
|
||||
|
||||
In other words, a *mixin* provides methods that implement a certain behavior, but we do not use it alone, we use it to add the behavior to other classes.
|
||||
|
||||
## 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.
|
||||
|
||||
For instance here the mixin `sayHiMixin` is used to add some "speech" for `User`:
|
||||
|
||||
```js run
|
||||
*!*
|
||||
// mixin
|
||||
*/!*
|
||||
let sayHiMixin = {
|
||||
sayHi() {
|
||||
alert(`Hello ${this.name}`);
|
||||
},
|
||||
sayBye() {
|
||||
alert(`Bye ${this.name}`);
|
||||
}
|
||||
};
|
||||
|
||||
*!*
|
||||
// usage:
|
||||
*/!*
|
||||
class User {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// copy the methods
|
||||
Object.assign(User.prototype, sayHiMixin);
|
||||
|
||||
// now User can say hi
|
||||
new User("Dude").sayHi(); // Hello Dude!
|
||||
```
|
||||
|
||||
There's no inheritance, but a simple method copying. So `User` may extend some other class and also include the mixin to "mix-in" the additional methods, like this:
|
||||
|
||||
```js
|
||||
class User extends Person {
|
||||
// ...
|
||||
}
|
||||
|
||||
Object.assign(User.prototype, sayHiMixin);
|
||||
```
|
||||
|
||||
Mixins can make use of inheritance inside themselves.
|
||||
|
||||
For instance, here `sayHiMixin` inherits from `sayMixin`:
|
||||
|
||||
```js run
|
||||
let sayMixin = {
|
||||
say(phrase) {
|
||||
alert(phrase);
|
||||
}
|
||||
};
|
||||
|
||||
let sayHiMixin = {
|
||||
__proto__: sayMixin, // (or we could use Object.create to set the prototype here)
|
||||
|
||||
sayHi() {
|
||||
*!*
|
||||
// call parent method
|
||||
*/!*
|
||||
super.say(`Hello ${this.name}`);
|
||||
},
|
||||
sayBye() {
|
||||
super.say(`Bye ${this.name}`);
|
||||
}
|
||||
};
|
||||
|
||||
class User {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// copy the methods
|
||||
Object.assign(User.prototype, sayHiMixin);
|
||||
|
||||
// now User can say hi
|
||||
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.
|
||||
|
||||

|
||||
|
||||
That's because methods from `sayHiMixin` have `[[HomeObject]]` set to it. So `super` actually means `sayHiMixin.__proto__`, not `User.__proto__`.
|
||||
|
||||
## EventMixin
|
||||
|
||||
Now let's make a mixin for real life.
|
||||
|
||||
The important feature of many objects is working with events.
|
||||
|
||||
That is: an object should have a method to "generate an event" when something important happens to it, and other objects should be able to "listen" to such events.
|
||||
|
||||
An event must have a name and, optionally, bundle some additional data.
|
||||
|
||||
For instance, an object `user` can generate an event `"login"` when the visitor logs in. And another object `calendar` may want to receive such events to load the calendar for the logged-in person.
|
||||
|
||||
Or, a `menu` can generate the event `"select"` when a menu item is selected, and other objects may want to get that information and react on that event.
|
||||
|
||||
Events is a way to "share information" with anyone who wants it. They can be useful in any class, so let's make a mixin for them:
|
||||
|
||||
```js run
|
||||
let eventMixin = {
|
||||
/**
|
||||
* Subscribe to event, usage:
|
||||
* menu.on('select', function(item) { ... }
|
||||
*/
|
||||
on(eventName, handler) {
|
||||
if (!this._eventHandlers) this._eventHandlers = {};
|
||||
if (!this._eventHandlers[eventName]) {
|
||||
this._eventHandlers[eventName] = [];
|
||||
}
|
||||
this._eventHandlers[eventName].push(handler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel the subscription, usage:
|
||||
* menu.off('select', handler)
|
||||
*/
|
||||
off(eventName, handler) {
|
||||
let handlers = this._eventHandlers && this._eventHandlers[eventName];
|
||||
if (!handlers) return;
|
||||
for (let i = 0; i < handlers.length; i++) {
|
||||
if (handlers[i] === handler) {
|
||||
handlers.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate the event and attach the data to it
|
||||
* this.trigger('select', data1, data2);
|
||||
*/
|
||||
trigger(eventName, ...args) {
|
||||
if (!this._eventHandlers || !this._eventHandlers[eventName]) {
|
||||
return; // no handlers for that event name
|
||||
}
|
||||
|
||||
// call the handlers
|
||||
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
There are 3 methods here:
|
||||
|
||||
1. `.on(eventName, handler)` -- assigns function `handler` to run when the event with that name happens. The handlers are stored in the `_eventHandlers` property.
|
||||
2. `.off(eventName, handler)` -- removes the function from the handlers list.
|
||||
3. `.trigger(eventName, ...args)` -- generates the event: all assigned handlers are called and `args` are passed as arguments to them.
|
||||
|
||||
|
||||
Usage:
|
||||
|
||||
```js run
|
||||
// Make a class
|
||||
class Menu {
|
||||
choose(value) {
|
||||
this.trigger("select", value);
|
||||
}
|
||||
}
|
||||
// Add the mixin
|
||||
Object.assign(Menu.prototype, eventMixin);
|
||||
|
||||
let menu = new Menu();
|
||||
|
||||
// call the handler on selection:
|
||||
*!*
|
||||
menu.on("select", value => alert(`Value selected: ${value}`));
|
||||
*/!*
|
||||
|
||||
// triggers the event => shows Value selected: 123
|
||||
menu.choose("123"); // value selected
|
||||
```
|
||||
|
||||
Now if we have the code interested to react on user selection, we can bind it with `menu.on(...)`.
|
||||
|
||||
And the `eventMixin` can add such behavior to as many classes as we'd like, without interfering with the inheritance chain.
|
||||
|
||||
## Summary
|
||||
|
||||
*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 them into the prototype.
|
||||
|
||||
We can use mixins as a way to augment a class by multiple behaviors, like event-handling as we have seen above.
|
||||
|
||||
Mixins may become a point of conflict if they occasionally overwrite native class methods. So generally one should think well about the naming for a mixin, to minimize such possibility.
|
43
1-js/09-classes/08-mixins/head.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
<script>
|
||||
let eventMixin = {
|
||||
|
||||
/**
|
||||
* Subscribe to event, usage:
|
||||
* menu.on('select', function(item) { ... }
|
||||
*/
|
||||
on(eventName, handler) {
|
||||
if (!this._eventHandlers) this._eventHandlers = {};
|
||||
if (!this._eventHandlers[eventName]) {
|
||||
this._eventHandlers[eventName] = [];
|
||||
}
|
||||
this._eventHandlers[eventName].push(handler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel the subscription, usage:
|
||||
* menu.off('select', handler)
|
||||
*/
|
||||
off(eventName, handler) {
|
||||
let handlers = this._eventHandlers && this._eventHandlers[eventName];
|
||||
if (!handlers) return;
|
||||
for(let i = 0; i < handlers.length; i++) {
|
||||
if (handlers[i] == handler) {
|
||||
handlers.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate the event and attach the data to it
|
||||
* this.trigger('select', data1, data2);
|
||||
*/
|
||||
trigger(eventName, ...args) {
|
||||
if (!this._eventHandlers || !this._eventHandlers[eventName]) {
|
||||
return; // no handlers for that event name
|
||||
}
|
||||
|
||||
// call the handlers
|
||||
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
|
||||
}
|
||||
};
|
||||
</script>
|
BIN
1-js/09-classes/08-mixins/mixin-inheritance.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
1-js/09-classes/08-mixins/mixin-inheritance@2x.png
Normal file
After Width: | Height: | Size: 68 KiB |
1
1-js/09-classes/index.md
Normal file
|
@ -0,0 +1 @@
|
|||
# Classes
|