en.javascript.info/1-js/8-oop/5-functional-inheritance/article.md
Ilya Kantor 87bf53d076 update
2014-11-16 01:40:20 +03:00

401 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 его можно реализовать несколькими путями, один из которых -- с использованием наложения конструкторов, мы рассмотрим в этой главе.
[cut]
## Зачем наследование?
Ранее мы обсуждали различные реализации кофеварки. Продолжим эту тему далее.
Хватит ли нам только кофеварки для удобной жизни? Вряд ли... Скорее всего, ещё понадобятся как минимум холодильник, микроволновка, а возможно и другие *машины*.
В реальной жизни у этих *машин* есть базовые правила пользования. Например, большая кнопка <i class="fa fa-power-off"></i> -- включение, шнур с розеткой нужно воткнуть в питание и т.п.
Можно сказать, что "у всех машин есть общие свойства, а конкретные машины могут их дополнять".
Именно поэтому, увидев новую технику, мы уже можем что-то с ней сделать, даже не читая инструкцию.
**Механизм наследования позволяет определить базовый класс `Машина`, в нём описать то, что свойственно всем машинам, а затем на его основе построить другие, более конкретные: `Кофеварка`, `Холодильник` и т.п.**
[smart header="В веб-разработке всё так же"]
В веб-разработке нам могут понадобиться классы `Меню`, `Табы`, `Диалог` и другие компоненты интерфейса.
Можно выделить полезный общий функционал в класс `Компонент` и наследовать их от него, чтобы не дублировать код. Это обычная практика, принятая во множестве библиотек.
[/smart]
## Наследование от Machine
Например, у нас есть класс `Machine`, который реализует методы "включить" `enable()` и "выключить" `disable()`:
```js
function Machine() {
var enabled = false;
this.enable = function() {
enabled = true;
};
this.disable = function() {
enabled = false;
};
}
```
Унаследуем от него кофеварку. При этом она получит эти методы автоматически:
```js
function CoffeeMachine(power) {
*!*
Machine.call(this);
*/!*
var waterAmount = 0;
this.setWaterAmount = function(amount) {
waterAmount = amount;
};
function onReady() {
alert('Кофе готово!');
}
this.run = function() {
setTimeout(onReady, 1000);
};
}
var coffeeMachine = new CoffeeMachine(10000);
coffeeMachine.enable();
```
Наследование реализовано вызовом `Machine.call(this)` в начале `CoffeeMachine`.
Он вызывает функцию `Machine`, передавая ей в качестве контекста `this` текущий объект. `Machine`, в процессе выполнения, записывает в `this` различные полезные свойства и методы, в нашем случае `this.enable` и `this.disable`.
Далее `CoffeeMachine` продолжает выполнение и может добавить свои свойства и методы, а также пользоваться унаследованными.
## Защищённые свойства
В коде выше есть одна проблема.
**Наследник не имеет доступа к приватным свойствам родителя.**
Иначе говоря, если кофеварка захочет обратиться к `enabled`, то её ждёт разочарование:
```js
//+ run
function Machine() {
var enabled = false;
this.enable = function() {
enabled = true;
};
this.disable = function() {
enabled = false;
};
}
function CoffeeMachine(power) {
Machine.call(this);
this.enable();
*!*
// ошибка, переменная не определена!
alert(enabled);
*/!*
}
var coffeeMachine = new CoffeeMachine(10000);
```
Это естественно, ведь `enabled` -- локальная переменная функции `Machine`. Она находится в другой области видимости.
**Чтобы наследник имел доступ к свойству, оно должно быть записано в `this`.**
**При этом, чтобы обозначить, что свойство является внутренним, его имя начинают с подчёркивания `_`.**
```js
//+ run
function Machine() {
*!*
this._enabled = false;
*/!*
this.enable = function() {
this._enabled = true;
};
this.disable = function() {
this._enabled = false;
};
}
function CoffeeMachine(power) {
Machine.call(this);
this.enable();
*!*
alert(this._enabled); // true
*/!*
}
var coffeeMachine = new CoffeeMachine(10000);
```
**Подчёркивание в начале свойства -- общепринятый знак, что свойство является внутренним, предназначенным лишь для доступа из самого объекта и его наследников. Такие свойства называют *защищёнными*.**
Технически это, конечно, возможно, но приличный программист снаружи в такое свойство не полезет.
**Вообще, это стандартная практика: конструктор сохраняет свои параметры в свойствах объекта. Иначе наследники не будут иметь к ним доступ.**
## Перенос свойства в защищённые
В коде выше есть свойство `power`. Сейчас мы его тоже сделаем защищённым и перенесём в `Machine`, поскольку "мощность" свойственна всем машинам, а не только кофеварке:
```js
//+ run
function CoffeeMachine(power) {
*!*
Machine.apply(this, arguments); // (1)
*/!*
alert(this._enabled); // false
alert(this._power); // 10000
}
function Machine(power) {
*!*
this._power = power; // (2)
*/!*
this._enabled = false;
this.enable = function() {
this._enabled = true;
};
this.disable = function() {
this._enabled = false;
};
}
var coffeeMachine = new CoffeeMachine(10000);
```
В коде выше при вызове `new CoffeeMachine(10000)` в строке `(1)` кофеварка передаёт аргументы и контекст родителю вызовом `Machine.apply(this, arguments)`.
Можно было бы использовать `Machine.call(this, power)`, но использование `apply` гарантирует передачу всех аргументов, мало ли, вдруг мы в будущем захотим их добавить.
Далее конструктор `Machine` в строке `(2)` сохраняет `power` в свойстве объекта `this._power`, благодаря этому кофеварка, когда наследование перейдёт обратно к `CoffeeMachine`, сможет сразу обращаться к нему.
## Переопределение методов
Итак, мы получили класс `CoffeeMachine`, который наследует от `Machine`.
Аналогичным образом мы можем унаследовать от `Machine` холодильник `Fridge`, микроволновку `MicroOven` и другие классы, которые разделяют общий "машинный" функционал.
Для этого достаточно вызвать `Machine` текущем контексте, а затем добавить свои методы.
```js
// Fridge может добавить и свои аргументы,
// которые в Machine не будут использованы
function Fridge(power, temperature) {
Machine.call(this, arguments);
// ...
}
```
Кроме создания новых методов, можно заменить унаследованные на свои:
```js
function CoffeeMachine(power, capacity) {
Machine.apply(this, arguments);
// переопределить this.enable
this.enable = function() {
/* enable для кофеварки */
};
}
```
...Однако, как правило, мы хотим не заменить, а *расширить* метод родителя. Например, сделать так, чтобы при включении кофеварка тут же запускалась.
Для этого метод родителя предварительно копируют в переменную, и затем вызывают внутри нового `enable` -- там, где считают нужным:
```js
function CoffeeMachine(power) {
Machine.apply(this, arguments);
*!*
var parentEnable = this.enable; // (1)
this.enable = function() { // (2)
parentEnable.call(this); // (3)
this.run(); // (4)
}
*/!*
...
}
```
**Общая схема переопределения метода (по строкам выделенного фрагмента кода):**
<ol>
<li>Мы скопировали доставшийся от родителя метод `enable` в переменную, например `parentEnable`.</li>
<li>Заменили метод `this.enable()` на свою функцию...</li>
<li>Которая по-прежнему реализует старый функционал через вызов `parentEnable`...</li>
<li>И в дополнение к нему делает что-то своё, например запускает приготовление кофе.</li>
</ol>
Обратим внимание на строку `(3)`.
В ней родительский метод вызывается так: `parentEnable.call(this)`. Если бы вызов был таким: `parentEnable()`, то ему бы не передался текущий `this` и возникла бы ошибка.
Технически, можно сделать возможность вызывать его и как `parentEnable()`, но тогда надо гарантировать, что контекст будет правильным, например привязать его при помощи `bind` или при объявлении, в родителе, вообще не использовать `this`, а получать контекст через замыкание, вот так:
```js
//+ run
function Machine(power) {
this._enabled = false;
*!*
var self = this;
this.enable = function() {
// используем внешнюю переменную вместо this
self._enabled = true;
};
*/!*
this.disable = function() {
this._enabled = false;
};
}
function CoffeeMachine(power) {
Machine.apply(this, arguments);
var waterAmount = 0;
this.setWaterAmount = function(amount) {
waterAmount = amount;
};
*!*
var parentEnable = this.enable;
this.enable = function() {
parentEnable(); // теперь можно вызывать как угодно, this не важен
this.run();
}
*/!*
function onReady() {
alert('Кофе готово!');
}
this.run = function() {
setTimeout(onReady, 1000);
};
}
var coffeeMachine = new CoffeeMachine(10000);
coffeeMachine.setWaterAmount(50);
coffeeMachine.enable();
```
В коде выше родительский метод `parentEnable = this.enable` успешно продолжает работать даже при вызове без контекста. А всё потому, что использует `self` внутри.
## Итого
Организация наследования, которая описана в этой главе, называется "функциональным паттерном наследования".
Её общая схема (кратко):
<ol>
<li>Объявляется конструктор родителя `Machine`. В нём могут быть приватные (private), публичные (public) и защищённые (protected) свойства:
```js
function Machine(params) {
// локальные переменные и функции доступны только внутри Machine
var private;
// публичные доступны снаружи
this.public = ...;
// защищённые доступны внутри Machine и для потомков
// мы договариваемся не трогать их снаружи
this._protected = ...
}
var machine = new Machine(...)
machine.public();
```
</li>
<li>Для наследования конструктор потомка вызывает родителя в своём контексте через `apply`. После чего может добавить свои переменные и методы:
```js
function CoffeeMachine(params) {
// универсальный вызов с передачей любых аргументов
*!*
Machine.apply(this, arguments);
*/!*
this.coffeePublic = ...
}
var coffeeMachine = new CoffeeMachine(...);
coffeeMachine.public();
coffeeMachine.coffeePublic();
```
</li>
<li>В `CoffeeMachine` свойства, полученные от родителя, можно перезаписать своими. Но обычно требуется не заменить, а расширить метод родителя. Для этого он предварительно копируется в переменную:
```js
function CoffeeMachine(params) {
Machine.apply(this, arguments);
*!*
var parentProtected = this._protected;
this._protected = function(args) {
parentProtected.call(this, args); // (*)
// ...
};
*/!*
}
```
Строку `(*)` можно упростить до `parentProtected(args)`, если метод родителя не использует `this`, а, например, привязан к `var self = this`:
```js
function Machine(params) {
var self = this;
this._protected = function() {
self.property = "value";
};
}
```
</li>
</ol>
**В следующих главах мы будем изучать прототипный подход, который обладаем рядом преимуществ, по сравнению с функциональным.**
Но функциональный тоже бывает полезен.
В своей практике разработки я обычно наследую функционально в тех случаях, когда *уже* есть какой-то код, который на нём построен. К примеру, уже существуют классы, написанные сторонними разработчиками, которые можно доопределить или расширить только так.