This commit is contained in:
Ilya Kantor 2014-11-16 01:40:20 +03:00
parent 962caebbb7
commit 87bf53d076
1825 changed files with 94929 additions and 0 deletions

View file

@ -0,0 +1,401 @@
# Функциональное наследование
Наследование -- это создание новых "классов" на основе существующих.
В 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>
**В следующих главах мы будем изучать прототипный подход, который обладаем рядом преимуществ, по сравнению с функциональным.**
Но функциональный тоже бывает полезен.
В своей практике разработки я обычно наследую функционально в тех случаях, когда *уже* есть какой-то код, который на нём построен. К примеру, уже существуют классы, написанные сторонними разработчиками, которые можно доопределить или расширить только так.