Skip to content

Commit 5c571ce

Browse files
authored
Merge pull request #1629 from Rnbsov/class-inheritance
Обновление статьи "Наследование классов" в соответствии с английской версией учебника.
2 parents c93800a + 0761a28 commit 5c571ce

File tree

2 files changed

+136
-57
lines changed

2 files changed

+136
-57
lines changed

1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
class ExtendedClock extends Clock {
22
constructor(options) {
33
super(options);
4-
let { precision=1000 } = options;
4+
let { precision = 1000 } = options;
55
this.precision = precision;
66
}
77

1-js/09-classes/02-class-inheritance/article.md

Lines changed: 135 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
12
# Наследование классов
23

3-
Допустим, у нас есть два класса.
4+
Наследование классов - это способ расширения одного класса другим классом.
5+
6+
Таким образом, мы можем добавить новый функционал к уже существующему.
7+
8+
## Ключевое слово "extends"
49

5-
`Animal`:
10+
Допустим, у нас есть класс `Animal`:
611

712
```js
813
class Animal {
@@ -16,57 +21,26 @@ class Animal {
1621
}
1722
stop() {
1823
this.speed = 0;
19-
alert(`${this.name} стоит.`);
24+
alert(`${this.name} стоит неподвижно.`);
2025
}
2126
}
2227

2328
let animal = new Animal("Мой питомец");
2429
```
2530

26-
![](rabbit-animal-independent-animal.svg)
27-
28-
...И `Rabbit`:
29-
30-
```js
31-
class Rabbit {
32-
constructor(name) {
33-
this.name = name;
34-
}
35-
hide() {
36-
alert(`${this.name} прячется!`);
37-
}
38-
}
39-
40-
let rabbit = new Rabbit("Мой кролик");
41-
```
31+
Вот как мы можем представить объект `animal` и класс `Animal` графически:
4232

43-
![](rabbit-animal-independent-rabbit.svg)
33+
![](rabbit-animal-independent-animal.svg)
4434

45-
Сейчас они полностью независимы.
35+
...И мы хотели бы создать ещё один `class Rabbit`.
4636

47-
Но мы хотим, чтобы `Rabbit` расширял `Animal`. Другими словами, кролики должны происходить от животных, т.е. иметь доступ к методам `Animal` и расширять функциональность `Animal` своими методами.
37+
Поскольку кролики - это животные, класс `Rabbit` должен быть основан на `Animal`, и иметь доступ к методам животных, так чтобы кролики могли делать то, что могут делать "общие" животные.
4838

49-
Для того, чтобы наследовать класс от другого, мы должны использовать ключевое слово `"extends"` и указать название родительского класса перед `{..}`.
39+
Синтаксис для расширения другого класса следующий: `class Child extends Parent`.
5040

51-
Ниже `Rabbit` наследует от `Animal`:
41+
Давайте создадим `class Rabbit`, который наследуется от `Animal`:
5242

53-
```js run
54-
class Animal {
55-
constructor(name) {
56-
this.speed = 0;
57-
this.name = name;
58-
}
59-
run(speed) {
60-
this.speed = speed;
61-
alert(`${this.name} бежит со скоростью ${this.speed}.`);
62-
}
63-
stop() {
64-
this.speed = 0;
65-
alert(`${this.name} стоит.`);
66-
}
67-
}
68-
69-
// Наследуем от Animal указывая "extends Animal"
43+
```js
7044
*!*
7145
class Rabbit extends Animal {
7246
*/!*
@@ -80,24 +54,30 @@ let rabbit = new Rabbit("Белый кролик");
8054
rabbit.run(5); // Белый кролик бежит со скоростью 5.
8155
rabbit.hide(); // Белый кролик прячется!
8256
```
83-
Теперь код `Rabbit` стал короче, так как используется конструктор класса `Animal` по умолчанию и кролик может использовать метод `run` как и все животные.
8457

85-
Ключевое слово `extends` работает, используя прототипы. Оно устанавливает `Rabbit.prototype.[[Prototype]]` в `Animal.prototype`. Так что если метод не найден в `Rabbit.prototype`, JavaScript берёт его из `Animal.prototype`.
58+
Объект класса `Rabbit` имеет доступ как к методам `Rabbit`, таким как `rabbit.hide()`, так и к методам `Animal`, таким как `rabbit.run()`.
59+
60+
Внутри ключевое слово `extends` работает по старой доброй механике прототипов. Оно устанавливает `Rabbit.prototype.[[Prototype]]` в `Animal.prototype`. Таким образом, если метода не оказалось в `Rabbit.prototype`, JavaScript берет его из `Animal.prototype`.
8661

8762
![](animal-rabbit-extends.svg)
8863

89-
Как мы помним из главы <info:native-prototypes>, в JavaScript используется наследование на прототипах для встроенных объектов. Например `Date.prototype.[[Prototype]]` это `Object.prototype`, поэтому у дат есть универсальные методы объекта.
64+
Например, чтобы найти метод `rabbit.run`, движок проверяет (снизу вверх на картинке):
65+
1. Объект `rabbit` (не имеет `run`).
66+
2. Его прототип, то есть `Rabbit.prototype` (имеет `hide`, но не имеет `run`).
67+
3. Его прототип, то есть (вследствие `extends`) `Animal.prototype`, в котором, наконец, есть метод `run`.
68+
69+
Как мы помним из главы <info:native-prototypes>, сам JavaScript использует наследование на прототипах для встроенных объектов. Например, `Date.prototype.[[Prototype]]` является `Object.prototype`, поэтому у дат есть универсальные методы объекта.
9070

9171
````smart header="После `extends` разрешены любые выражения"
92-
Синтаксис создания класса допускает указывать после `extends` не только класс, но любое выражение.
72+
Синтаксис создания класса допускает указывать после `extends` не только класс, но и любое выражение.
9373

9474
Пример вызова функции, которая генерирует родительский класс:
9575

9676
```js run
9777
function f(phrase) {
9878
return class {
99-
sayHi() { alert(phrase) }
100-
}
79+
sayHi() { alert(phrase); }
80+
};
10181
}
10282

10383
*!*
@@ -113,23 +93,25 @@ new User().sayHi(); // Привет
11393
11494
## Переопределение методов
11595
116-
Давайте пойдём дальше и переопределим метод. Сейчас `Rabbit` наследует от `Animal` метод `stop`, который устанавливает `this.speed = 0`.
96+
Теперь давайте продвинемся дальше и переопределим метод. По умолчанию все методы, не указанные в классе `Rabbit`, берутся непосредственно "как есть" из класса `Animal`.
11797
118-
Если мы определим свой метод `stop` в классе `Rabbit`, то он будет использоваться взамен родительского:
98+
Но если мы укажем в `Rabbit` собственный метод, например `stop()`, то он будет использован вместо него:
11999
120100
```js
121101
class Rabbit extends Animal {
122102
stop() {
123-
// ...будет использован для rabbit.stop()
103+
// ...теперь это будет использоваться для rabbit.stop()
104+
// вместо stop() из класса Animal
124105
}
125106
}
126107
```
127108
128-
...Впрочем, обычно мы не хотим полностью заменить родительский метод, а скорее хотим сделать новый на его основе, изменяя или расширяя его функциональность. Мы делаем что-то в нашем методе и вызываем родительский метод до/после или в процессе.
109+
Впрочем, обычно мы не хотим полностью заменить родительский метод, а скорее хотим сделать новый на его основе, изменяя или расширяя его функциональность. Мы делаем что-то в нашем методе и вызываем родительский метод до/после или в процессе.
129110
130111
У классов есть ключевое слово `"super"` для таких случаев.
112+
131113
- `super.method(...)` вызывает родительский метод.
132-
- `super(...)` вызывает родительский конструктор (работает только внутри нашего конструктора).
114+
- `super(...)` для вызова родительского конструктора (работает только внутри нашего конструктора).
133115
134116
Пусть наш кролик автоматически прячется при остановке:
135117
@@ -148,7 +130,7 @@ class Animal {
148130
149131
stop() {
150132
this.speed = 0;
151-
alert(`${this.name} стоит.`);
133+
alert(`${this.name} стоит неподвижно.`);
152134
}
153135
154136
}
@@ -178,6 +160,7 @@ rabbit.stop(); // Белый кролик стоит. Белый кролик п
178160
Как упоминалось в главе <info:arrow-functions>, стрелочные функции не имеют `super`.
179161
180162
При обращении к `super` стрелочной функции он берётся из внешней функции:
163+
181164
```js
182165
class Rabbit extends Animal {
183166
stop() {
@@ -247,11 +230,13 @@ let rabbit = new Rabbit("Белый кролик", 10); // Error: this is not de
247230

248231
Упс! При создании кролика - ошибка! Что не так?
249232

250-
Если коротко, то в классах-потомках конструктор обязан вызывать `super(...)`, и (!) делать это перед использованием `this`.
233+
Если коротко, то:
234+
235+
- **Конструкторы в наследуемых классах должны обязательно вызывать `super(...)`, и (!) делать это перед использованием `this`.**.
251236

252237
...Но почему? Что происходит? Это требование кажется довольно странным.
253238

254-
Конечно, всему есть объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит.
239+
Конечно, всему есть своё объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит.
255240

256241
В JavaScript существует различие между "функцией-конструктором наследующего класса" и всеми остальными. В наследующем классе соответствующая функция-конструктор помечена специальным внутренним свойством `[[ConstructorKind]]:"derived"`.
257242

@@ -295,6 +280,100 @@ alert(rabbit.earLength); // 10
295280
*/!*
296281
```
297282

283+
### Переопределение полей класса: тонкое замечание
284+
285+
```warn header="Продвинутое замечание"
286+
В этом подразделе предполагается, что у вас уже есть определённый опыт работы с классами, возможно, в других языках программирования.
287+
288+
Это даёт лучшее представление о языке, а также объясняет поведение, которое может быть источником ошибок (но не очень часто).
289+
290+
Если вы считаете этот материал слишком трудным для понимания, просто продолжайте читать дальше, а затем вернитесь к нему через некоторое время.
291+
```
292+
293+
Мы можем переопределять не только методы, но и поля класса.
294+
295+
Однако, когда мы получаем доступ к переопределенному полю в родительском конструкторе, это поведение отличается от большинства других языков программирования.
296+
297+
Рассмотрим этот пример:
298+
299+
```js run
300+
class Animal {
301+
name = 'animal';
302+
303+
constructor() {
304+
alert(this.name); // (*)
305+
}
306+
}
307+
308+
class Rabbit extends Animal {
309+
name = 'rabbit';
310+
}
311+
312+
new Animal(); // animal
313+
*!*
314+
new Rabbit(); // animal
315+
*/!*
316+
```
317+
318+
Здесь, класс `Rabbit` расширяет `Animal` и переопределяет поле `name` своим собственным значением.
319+
320+
В `Rabbit` нет собственного конструктора, поэтому вызывается конструктор `Animal`.
321+
322+
Что интересно, в обоих случаях: `new Animal()` и `new Rabbit()`, `alert` в строке `(*)` показывает `animal`.
323+
324+
**Другими словами, родительский конструктор всегда использует своё собственное значение поля, а не переопределённое.**
325+
326+
Что же в этом странного?
327+
328+
Если это ещё не ясно, сравните с методами.
329+
330+
Вот тот же код, но вместо поля `this.name`, мы вызываем метод `this.showName()`:
331+
332+
```js run
333+
class Animal {
334+
showName() { // вместо this.name = 'animal'
335+
alert('animal');
336+
}
337+
338+
constructor() {
339+
this.showName(); // вместо alert(this.name);
340+
}
341+
}
342+
343+
class Rabbit extends Animal {
344+
showName() {
345+
alert('rabbit');
346+
}
347+
}
348+
349+
new Animal(); // animal
350+
*!*
351+
new Rabbit(); // rabbit
352+
*/!*
353+
```
354+
355+
Обратите внимание: теперь результат другой.
356+
357+
И это то, чего мы, естественно, ожидаем. Когда родительский конструктор вызывается в производном классе, он использует переопределённый метод.
358+
359+
...Но для полей класса это не так. Как уже было сказано, родительский конструктор всегда использует родительское поле.
360+
361+
Почему же наблюдается разница?
362+
363+
Что ж, причина заключается в порядке инициализации полей. Поле класса инициализируется:
364+
- Перед конструктором для базового класса (который ничего не расширяет),
365+
- Сразу после `super()` для производного класса.
366+
367+
В нашем случае `Rabbit` - это производный класс. В нем нет конструктора `constructor()`. Как было сказано ранее, это то же самое, как если бы был пустой конструктор, содержащий только `super(...args)`.
368+
369+
Итак, `new Rabbit()` вызывает `super()`, таким образом, выполняя родительский конструктор, и (согласно правилу для производных классов) только после этого инициализируются поля его класса. На момент выполнения родительского конструктора ещё нет полей класса `Rabbit`, поэтому используются поля `Animal`.
370+
371+
Это тонкое различие между полями и методами характерно для JavaScript.
372+
373+
К счастью, такое поведение проявляется только в том случае, когда переопределенное поле используется в родительском конструкторе. Тогда может быть трудно понять, что происходит, поэтому мы объясняем это здесь.
374+
375+
Если это становится проблемой, её можно решить, используя методы или геттеры/сеттеры вместо полей.
376+
298377
## Устройство super, [[HomeObject]]
299378

300379
```warn header="Продвинутая информация"
@@ -316,6 +395,7 @@ alert(rabbit.earLength); // 10
316395
Вы можете пропустить эту часть и перейти ниже к подсекции `[[HomeObject]]`, если не хотите знать детали. Вреда не будет. Или читайте далее, если хотите разобраться.
317396

318397
В примере ниже `rabbit.__proto__ = animal`. Попробуем в `rabbit.eat()` вызвать `animal.eat()`, используя `this.__proto__`:
398+
319399
```js run
320400
let animal = {
321401
name: "Animal",
@@ -417,7 +497,6 @@ longEar.eat(); // Error: Maximum call stack size exceeded
417497

418498
Давайте посмотрим, как это работает - опять же, используя простые объекты:
419499

420-
421500
```js run
422501
let animal = {
423502
name: "Животное",

0 commit comments

Comments
 (0)