You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Наследование классов - это способ расширения одного класса другим классом.
5
+
6
+
Таким образом, мы можем добавить новый функционал к уже существующему.
7
+
8
+
## Ключевое слово "extends"
4
9
5
-
`Animal`:
10
+
Допустим, у нас есть класс `Animal`:
6
11
7
12
```js
8
13
classAnimal {
@@ -16,57 +21,26 @@ class Animal {
16
21
}
17
22
stop() {
18
23
this.speed=0;
19
-
alert(`${this.name} стоит.`);
24
+
alert(`${this.name} стоит неподвижно.`);
20
25
}
21
26
}
22
27
23
28
let animal =newAnimal("Мой питомец");
24
29
```
25
30
26
-

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

33
+

44
34
45
-
Сейчас они полностью независимы.
35
+
...И мы хотели бы создать ещё один `class Rabbit`.
46
36
47
-
Но мы хотим, чтобы`Rabbit`расширял `Animal`. Другими словами, кролики должны происходить от животных, т.е. иметь доступ к методам `Animal` и расширять функциональность `Animal` своими методами.
37
+
Поскольку кролики - это животные, класс`Rabbit`должен быть основан на `Animal`, и иметь доступ к методам животных, так чтобы кролики могли делать то, что могут делать "общие" животные.
48
38
49
-
Для того, чтобы наследовать класс от другого, мы должны использовать ключевое слово `"extends"` и указать название родительского класса перед `{..}`.
39
+
Синтаксис для расширения другого класса следующий: `class Child extends Parent`.
50
40
51
-
Ниже `Rabbit` наследует от `Animal`:
41
+
Давайте создадим `class Rabbit`, который наследуется от `Animal`:
52
42
53
-
```js run
54
-
classAnimal {
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
70
44
*!*
71
45
classRabbitextendsAnimal {
72
46
*/!*
@@ -80,24 +54,30 @@ let rabbit = new Rabbit("Белый кролик");
80
54
rabbit.run(5); // Белый кролик бежит со скоростью 5.
81
55
rabbit.hide(); // Белый кролик прячется!
82
56
```
83
-
Теперь код `Rabbit` стал короче, так как используется конструктор класса `Animal` по умолчанию и кролик может использовать метод `run` как и все животные.
84
57
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`.
86
61
87
62

88
63
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`, поэтому у дат есть универсальные методы объекта.
Синтаксис создания класса допускает указывать после `extends` не только класс, но любое выражение.
72
+
Синтаксис создания класса допускает указывать после `extends` не только класс, но и любое выражение.
93
73
94
74
Пример вызова функции, которая генерирует родительский класс:
95
75
96
76
```js run
97
77
functionf(phrase) {
98
78
returnclass {
99
-
sayHi() { alert(phrase) }
100
-
}
79
+
sayHi() { alert(phrase); }
80
+
};
101
81
}
102
82
103
83
*!*
@@ -113,23 +93,25 @@ new User().sayHi(); // Привет
113
93
114
94
## Переопределение методов
115
95
116
-
Давайте пойдём дальше и переопределим метод. Сейчас `Rabbit` наследует от `Animal` метод `stop`, который устанавливает `this.speed = 0`.
96
+
Теперь давайте продвинемся дальше и переопределим метод. По умолчанию все методы, не указанные в классе `Rabbit`, берутся непосредственно "как есть" из класса `Animal`.
117
97
118
-
Если мы определим свой метод `stop` в классе `Rabbit`, то он будет использоваться взамен родительского:
98
+
Но если мы укажем в `Rabbit` собственный метод, например `stop()`, то он будет использован вместо него:
119
99
120
100
```js
121
101
class Rabbit extends Animal {
122
102
stop() {
123
-
// ...будет использован для rabbit.stop()
103
+
// ...теперь это будет использоваться для rabbit.stop()
104
+
// вместо stop() из класса Animal
124
105
}
125
106
}
126
107
```
127
108
128
-
...Впрочем, обычно мы не хотим полностью заменить родительский метод, а скорее хотим сделать новый на его основе, изменяя или расширяя его функциональность. Мы делаем что-то в нашем методе и вызываем родительский метод до/после или в процессе.
109
+
Впрочем, обычно мы не хотим полностью заменить родительский метод, а скорее хотим сделать новый на его основе, изменяя или расширяя его функциональность. Мы делаем что-то в нашем методе и вызываем родительский метод до/после или в процессе.
129
110
130
111
У классов есть ключевое слово `"super"` для таких случаев.
112
+
131
113
- `super.method(...)` вызывает родительский метод.
132
-
- `super(...)` вызывает родительский конструктор (работает только внутри нашего конструктора).
114
+
- `super(...)` для вызова родительского конструктора (работает только внутри нашего конструктора).
133
115
134
116
Пусть наш кролик автоматически прячется при остановке:
135
117
@@ -148,7 +130,7 @@ class Animal {
148
130
149
131
stop() {
150
132
this.speed = 0;
151
-
alert(`${this.name} стоит.`);
133
+
alert(`${this.name} стоит неподвижно.`);
152
134
}
153
135
154
136
}
@@ -178,6 +160,7 @@ rabbit.stop(); // Белый кролик стоит. Белый кролик п
178
160
Как упоминалось в главе <info:arrow-functions>, стрелочные функции не имеют `super`.
179
161
180
162
При обращении к `super` стрелочной функции он берётся из внешней функции:
163
+
181
164
```js
182
165
class Rabbit extends Animal {
183
166
stop() {
@@ -247,11 +230,13 @@ let rabbit = new Rabbit("Белый кролик", 10); // Error: this is not de
247
230
248
231
Упс! При создании кролика - ошибка! Что не так?
249
232
250
-
Если коротко, то в классах-потомках конструктор обязан вызывать `super(...)`, и (!) делать это перед использованием `this`.
233
+
Если коротко, то:
234
+
235
+
-**Конструкторы в наследуемых классах должны обязательно вызывать `super(...)`, и (!) делать это перед использованием `this`.**.
251
236
252
237
...Но почему? Что происходит? Это требование кажется довольно странным.
253
238
254
-
Конечно, всему есть объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит.
239
+
Конечно, всему есть своё объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит.
255
240
256
241
В JavaScript существует различие между "функцией-конструктором наследующего класса" и всеми остальными. В наследующем классе соответствующая функция-конструктор помечена специальным внутренним свойством `[[ConstructorKind]]:"derived"`.
В этом подразделе предполагается, что у вас уже есть определённый опыт работы с классами, возможно, в других языках программирования.
287
+
288
+
Это даёт лучшее представление о языке, а также объясняет поведение, которое может быть источником ошибок (но не очень часто).
289
+
290
+
Если вы считаете этот материал слишком трудным для понимания, просто продолжайте читать дальше, а затем вернитесь к нему через некоторое время.
291
+
```
292
+
293
+
Мы можем переопределять не только методы, но и поля класса.
294
+
295
+
Однако, когда мы получаем доступ к переопределенному полю в родительском конструкторе, это поведение отличается от большинства других языков программирования.
296
+
297
+
Рассмотрим этот пример:
298
+
299
+
```js run
300
+
classAnimal {
301
+
name ='animal';
302
+
303
+
constructor() {
304
+
alert(this.name); // (*)
305
+
}
306
+
}
307
+
308
+
classRabbitextendsAnimal {
309
+
name ='rabbit';
310
+
}
311
+
312
+
newAnimal(); // animal
313
+
*!*
314
+
newRabbit(); // 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
+
classAnimal {
334
+
showName() { // вместо this.name = 'animal'
335
+
alert('animal');
336
+
}
337
+
338
+
constructor() {
339
+
this.showName(); // вместо alert(this.name);
340
+
}
341
+
}
342
+
343
+
classRabbitextendsAnimal {
344
+
showName() {
345
+
alert('rabbit');
346
+
}
347
+
}
348
+
349
+
newAnimal(); // animal
350
+
*!*
351
+
newRabbit(); // 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
+
Если это становится проблемой, её можно решить, используя методы или геттеры/сеттеры вместо полей.
Вы можете пропустить эту часть и перейти ниже к подсекции `[[HomeObject]]`, если не хотите знать детали. Вреда не будет. Или читайте далее, если хотите разобраться.
317
396
318
397
В примере ниже `rabbit.__proto__ = animal`. Попробуем в `rabbit.eat()` вызвать `animal.eat()`, используя `this.__proto__`:
0 commit comments