en.javascript.info/1-js/9-object-inheritance/15-class-inheritance/article.md
Ilya Kantor d4c714cbe1 work
2016-08-05 16:53:08 +03:00

340 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Наследование классов в JavaScript
Наследование на уровне объектов в JavaScript, как мы видели, реализуется через ссылку `__proto__`.
Теперь поговорим о наследовании на уровне классов, то есть когда объекты, создаваемые, к примеру, через `new Admin`, должны иметь все методы, которые есть у объектов, создаваемых через `new User`, и ещё какие-то свои.
[cut]
## Наследование Array от Object
Для реализации наследования в наших классах мы будем использовать тот же подход, который принят внутри JavaScript.
Взглянем на него ещё раз на примере `Array`, который наследует от `Object`:
![](class-inheritance-array-object.png)
- Методы массивов `Array` хранятся в `Array.prototype`.
- `Array.prototype` имеет прототипом `Object.prototype`.
Поэтому когда экземпляры класса `Array` хотят получить метод массива -- они берут его из своего прототипа, например `Array.prototype.slice`.
Если же нужен метод объекта, например, `hasOwnProperty`, то его в `Array.prototype` нет, и он берётся из `Object.prototype`.
Отличный способ "потрогать это руками" -- запустить в консоли команду `console.dir([1,2,3])`.
Вывод в Chrome будет примерно таким:
![](console_dir_array.png)
Здесь отчётливо видно, что сами данные и `length` находятся в массиве, дальше в `__proto__` идут методы для массивов `concat`, то есть `Array.prototype`, а далее -- `Object.prototype`.
```smart header="`console.dir` для доступа к свойствам"
Обратите внимание, я использовал именно `console.dir`, а не `console.log`, поскольку `log` зачастую выводит объект в виде строки, без доступа к свойствам.
```
## Наследование в наших классах
Применим тот же подход для наших классов: объявим класс `Rabbit`, который будет наследовать от `Animal`.
Вначале создадим два этих класса по отдельности, они пока что будут совершенно независимы.
`Animal`:
```js
function Animal(name) {
this.name = name;
this.speed = 0;
}
Animal.prototype.run = function(speed) {
this.speed += speed;
alert( this.name + ' бежит, скорость ' + this.speed );
};
Animal.prototype.stop = function() {
this.speed = 0;
alert( this.name + ' стоит' );
};
```
`Rabbit`:
```js
function Rabbit(name) {
this.name = name;
this.speed = 0;
}
Rabbit.prototype.jump = function() {
this.speed++;
alert( this.name + ' прыгает' );
};
var rabbit = new Rabbit('Кроль');
```
Для того, чтобы наследование работало, объект `rabbit = new Rabbit` должен использовать свойства и методы из своего прототипа `Rabbit.prototype`, а если их там нет, то -- свойства и метода родителя, которые хранятся в `Animal.prototype`.
Если ещё короче -- порядок поиска свойств и методов должен быть таким: `rabbit -> Rabbit.prototype -> Animal.prototype`, по аналогии с тем, как это сделано для объектов и массивов.
Для этого можно поставить ссылку `__proto__` с `Rabbit.prototype` на `Animal.prototype`.
Можно сделать это так:
```js
Rabbit.prototype.__proto__ = Animal.prototype;
```
Однако, прямой доступ к `__proto__` не поддерживается в IE10-, поэтому для поддержки этих браузеров мы используем функцию `Object.create`. Она либо встроена либо легко эмулируется во всех браузерах.
Класс `Animal` остаётся без изменений, а `Rabbit.prototype` мы будем создавать с нужным прототипом, используя `Object.create`:
```js no-beautify
function Rabbit(name) {
this.name = name;
this.speed = 0;
}
*!*
// задаём наследование
Rabbit.prototype = Object.create(Animal.prototype);
*/!*
// и добавим свой метод (или методы...)
Rabbit.prototype.jump = function() { ... };
```
Теперь выглядеть иерархия будет так:
![](class-inheritance-rabbit-animal.png)
В `prototype` по умолчанию всегда находится свойство `constructor`, указывающее на функцию-конструктор. В частности, `Rabbit.prototype.constructor == Rabbit`. Если мы рассчитываем использовать это свойство, то при замене `prototype` через `Object.create` нужно его явно сохранить:
```js
Rabbit.prototype = Object.create(Animal.prototype);
Rabbit.prototype.constructor = Rabbit;
```
## Полный код наследования
Для наглядности -- вот итоговый код с двумя классами `Animal` и `Rabbit`:
```js
// 1. Конструктор Animal
function Animal(name) {
this.name = name;
this.speed = 0;
}
// 1.1. Методы -- в прототип
Animal.prototype.stop = function() {
this.speed = 0;
alert( this.name + ' стоит' );
}
Animal.prototype.run = function(speed) {
this.speed += speed;
alert( this.name + ' бежит, скорость ' + this.speed );
};
// 2. Конструктор Rabbit
function Rabbit(name) {
this.name = name;
this.speed = 0;
}
// 2.1. Наследование
Rabbit.prototype = Object.create(Animal.prototype);
Rabbit.prototype.constructor = Rabbit;
// 2.2. Методы Rabbit
Rabbit.prototype.jump = function() {
this.speed++;
alert( this.name + ' прыгает, скорость ' + this.speed );
}
```
Как видно, наследование задаётся всего одной строчкой, поставленной в правильном месте.
Обратим внимание: `Rabbit.prototype = Object.create(Animal.prototype)` присваивается сразу после объявления конструктора, иначе он перезатрёт уже записанные в прототип методы.
````warn header="Неправильный вариант: `Rabbit.prototype = new Animal`"
В некоторых устаревших руководствах предлагают вместо `Object.create(Animal.prototype)` записывать в прототип `new Animal`, вот так:
```js
// вместо Rabbit.prototype = Object.create(Animal.prototype)
Rabbit.prototype = new Animal();
```
Частично, он рабочий, поскольку иерархия прототипов будет такая же, ведь `new Animal` -- это объект с прототипом `Animal.prototype`, как и `Object.create(Animal.prototype)`. Они в этом плане идентичны.
Но у этого подхода важный недостаток. Как правило мы не хотим создавать `Animal`, а хотим только унаследовать его методы!
Более того, на практике создание объекта может требовать обязательных аргументов, влиять на страницу в браузере, делать запросы к серверу и что-то ещё, чего мы хотели бы избежать. Поэтому рекомендуется использовать вариант с `Object.create`.
````
## Вызов конструктора родителя
Посмотрим внимательно на конструкторы `Animal` и `Rabbit` из примеров выше:
```js
function Animal(name) {
this.name = name;
this.speed = 0;
}
function Rabbit(name) {
this.name = name;
this.speed = 0;
}
```
Как видно, объект `Rabbit` не добавляет никакой особенной логики при создании, которой не было в `Animal`.
Чтобы упростить поддержку кода, имеет смысл не дублировать код конструктора `Animal`, а напрямую вызвать его:
```js
function Rabbit(name) {
Animal.apply(this, arguments);
}
```
Такой вызов запустит функцию `Animal` в контексте текущего объекта, со всеми аргументами, она выполнится и запишет в `this` всё, что нужно.
Здесь можно было бы использовать и `Animal.call(this, name)`, но `apply` надёжнее, так как работает с любым количеством аргументов.
## Переопределение метода
Итак, `Rabbit` наследует `Animal`. Теперь если какого-то метода нет в `Rabbit.prototype` -- он будет взят из `Animal.prototype`.
В `Rabbit` может понадобиться задать какие-то методы, которые у родителя уже есть. Например, кролики бегают не так, как остальные животные, поэтому переопределим метод `run()`:
```js
Rabbit.prototype.run = function(speed) {
this.speed++;
this.jump();
};
```
Вызов `rabbit.run()` теперь будет брать `run` из своего прототипа:
![](class-inheritance-rabbit-run-animal.png)
### Вызов метода родителя внутри своего
Более частая ситуация -- когда мы хотим не просто заменить метод на свой, а взять метод родителя и расширить его. Скажем, кролик бежит так же, как и другие звери, но время от времени подпрыгивает.
Для вызова метода родителя можно обратиться к нему напрямую, взяв из прототипа:
```js
Rabbit.prototype.run = function() {
*!*
// вызвать метод родителя, передав ему текущие аргументы
Animal.prototype.run.apply(this, arguments);
*/!*
this.jump();
}
```
Обратите внимание на вызов через `apply` и явное указание контекста.
Если вызвать просто `Animal.prototype.run()`, то в качестве `this` функция `run` получит `Animal.prototype`, а это неверно, нужен текущий объект.
## Итого
- Для наследования нужно, чтобы "склад методов потомка" (`Child.prototype`) наследовал от "склада метода родителей" (`Parent.prototype`).
Это можно сделать при помощи `Object.create`:
Код:
```js
Rabbit.prototype = Object.create(Animal.prototype);
```
- Для того, чтобы наследник создавался так же, как и родитель, он вызывает конструктор родителя в своём контексте, используя `apply(this, arguments)`, вот так:
```js
function Rabbit(...) {
Animal.apply(this, arguments);
}
```
- При переопределении метода родителя в потомке, к исходному методу можно обратиться, взяв его напрямую из прототипа:
```js
Rabbit.prototype.run = function() {
var result = Animal.prototype.run.apply(this, ...);
// result -- результат вызова метода родителя
}
```
Структура наследования полностью:
```js run
*!*
// --------- Класс-Родитель ------------
*/!*
// Конструктор родителя пишет свойства конкретного объекта
function Animal(name) {
this.name = name;
this.speed = 0;
}
// Методы хранятся в прототипе
Animal.prototype.run = function() {
alert(this.name + " бежит!")
}
*!*
// --------- Класс-потомок -----------
*/!*
// Конструктор потомка
function Rabbit(name) {
Animal.apply(this, arguments);
}
// Унаследовать
*!*
Rabbit.prototype = Object.create(Animal.prototype);
*/!*
// Желательно и constructor сохранить
Rabbit.prototype.constructor = Rabbit;
// Методы потомка
Rabbit.prototype.run = function() {
// Вызов метода родителя внутри своего
Animal.prototype.run.apply(this);
alert( this.name + " подпрыгивает!" );
};
// Готово, можно создавать объекты
var rabbit = new Rabbit('Кроль');
rabbit.run();
```
Такое наследование лучше функционального стиля, так как не дублирует методы в каждом объекте.
Кроме того, есть ещё неявное, но очень важное архитектурное отличие.
Зачастую вызов конструктора имеет какие-то побочные эффекты, например влияет на документ. Если конструктор родителя имеет какое-то поведение, которое нужно переопределить в потомке, то в функциональном стиле это невозможно.
Иначе говоря, в функциональном стиле в процессе создания `Rabbit` нужно обязательно вызывать `Animal.apply(this, arguments)`, чтобы получить методы родителя -- и если этот `Animal.apply` кроме добавления методов говорит: "Му-у-у!", то это проблема:
```js
function Animal() {
this.walk = function() {
alert('walk')
};
alert( 'Му-у-у!' );
}
function Rabbit() {
Animal.apply(this, arguments); // как избавиться от мычания, но получить walk?
}
```
...Которой нет в прототипном подходе, потому что в процессе создания `new Rabbit` мы вовсе не обязаны вызывать конструктор родителя. Ведь методы находятся в прототипе.
Поэтому прототипный подход стоит предпочитать функциональному как более быстрый и универсальный. А что касается красоты синтаксиса -- она сильно лучше в новом стандарте ES6, которым можно пользоваться уже сейчас, если взять транслятор [babeljs](https://babeljs.io/).