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