Какое высказывание справедливо для классов при наследовании

Обновлено: 22.11.2024

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Наследование – это создание новых «классов» на основе существующих.

В JavaScript его можно реализовать несколькими путями, один из которых – с использованием наложения конструкторов, мы рассмотрим в этой главе.

Зачем наследование?

Ранее мы обсуждали различные реализации кофеварки. Продолжим эту тему далее.

Хватит ли нам только кофеварки для удобной жизни? Вряд ли… Скорее всего, ещё понадобятся как минимум холодильник, микроволновка, а возможно и другие машины.

В реальной жизни у этих машин есть базовые правила пользования. Например, большая кнопка – включение, шнур с розеткой нужно воткнуть в питание и т.п.

Можно сказать, что «у всех машин есть общие свойства, а конкретные машины могут их дополнять».

Именно поэтому, увидев новую технику, мы уже можем что-то с ней сделать, даже не читая инструкцию.

Механизм наследования позволяет определить базовый класс Машина , в нём описать то, что свойственно всем машинам, а затем на его основе построить другие, более конкретные: Кофеварка , Холодильник и т.п.

В веб-разработке всё так же

В веб-разработке нам могут понадобиться классы Меню , Табы , Диалог и другие компоненты интерфейса. В них всех обычно есть что-то общее.

Можно выделить такую общую функциональность в класс Компонент и наследовать их от него, чтобы не дублировать код.

Наследование от Machine

Базовый класс «машина» Machine будет реализовывать общего вида методы «включить» enable() и «выключить» disable() :

Унаследуем от него кофеварку. При этом она получит эти методы автоматически:

Наследование реализовано вызовом Machine.call(this) в начале конструктора CoffeeMachine .

Он вызывает функцию Machine , передавая ей в качестве контекста this текущий объект. Machine , в процессе выполнения, записывает в this различные полезные свойства и методы, в нашем случае this.enable и this.disable .

Далее конструктор CoffeeMachine продолжает выполнение и может добавить свои свойства и методы.

В результате мы получаем объект coffeeMachine , который включает в себя методы из Machine и CoffeeMachine .

Защищённые свойства

В коде выше есть одна проблема.

Наследник не имеет доступа к приватным свойствам родителя.

Иначе говоря, если кофеварка захочет обратиться к enabled , то её ждёт разочарование:

Это естественно, ведь enabled – локальная переменная функции Machine . Она находится в другой области видимости.

Чтобы наследник имел доступ к свойству, оно должно быть записано в this .

При этом, чтобы обозначить, что свойство является внутренним, его имя начинают с подчёркивания _ .

Подчёркивание в начале свойства – общепринятый знак, что свойство является внутренним, предназначенным лишь для доступа из самого объекта и его наследников. Такие свойства называют защищёнными.

Технически, залезть в него из внешнего кода, конечно, возможно, но приличный программист так делать не будет.

Перенос свойства в защищённые

У CoffeeMachine есть приватное свойство power . Сейчас мы его тоже сделаем защищённым и перенесём в Machine , поскольку «мощность» свойственна всем машинам, а не только кофеварке.

Теперь все машины Machine имеют мощность power . Обратим внимание, что мы из параметра конструктора сразу скопировали её в объект в строке (1) . Иначе она была бы недоступна из наследников.

В строке (2) мы теперь вызываем не просто Machine.call(this) , а расширенный вариант: Machine.apply(this, arguments) , который вызывает Machine в текущем контексте вместе с передачей текущих аргументов.

Можно было бы использовать и более простой вызов Machine.call(this, power) , но использование apply гарантирует передачу всех аргументов, вдруг их количество увеличится – не надо будет переписывать.

Переопределение методов

Итак, мы получили класс CoffeeMachine , который наследует от Machine .

Аналогичным образом мы можем унаследовать от Machine холодильник Fridge , микроволновку MicroOven и другие классы, которые разделяют общую «машинную» функциональность, то есть имеют мощность и их можно включать/выключать.

Для этого достаточно вызвать Machine в текущем контексте, а затем добавить свои методы.

Бывает так, что реализация конкретного метода машины в наследнике имеет свои особенности.

Можно, конечно, объявить в CoffeeMachine свой enable :

…Однако, как правило, мы хотим не заменить, а расширить метод родителя, добавить к нему что-то. Например, сделать так, чтобы при включении кофеварка тут же запускалась.

Для этого метод родителя предварительно копируют в переменную, и затем вызывают внутри нового enable – там, где считают нужным:

Общая схема переопределения метода (по строкам выделенного фрагмента кода):

  1. Копируем доставшийся от родителя метод this.enable в переменную, например parentEnable .
  2. Заменяем this.enable на свою функцию…
  3. …Которая по-прежнему реализует старую функциональность через вызов parentEnable .
  4. …И в дополнение к нему делает что-то своё, например запускает приготовление кофе.

Обратим внимание на строку (3) .

В ней родительский метод вызывается так: parentEnable.call(this) . Если бы вызов был таким: parentEnable() , то ему бы не передался текущий this и возникла бы ошибка.

Технически, можно сделать возможность вызывать его и как parentEnable() , но тогда надо гарантировать, что контекст будет правильным, например привязать его при помощи bind или при объявлении, в родителе, вообще не использовать this , а получать контекст через замыкание, вот так:

В коде выше родительский метод parentEnable = this.enable успешно продолжает работать даже при вызове без контекста. А всё потому, что использует self внутри.

Итого

Организация наследования, которая описана в этой главе, называется «функциональным паттерном наследования».

Её общая схема (кратко):

Объявляется конструктор родителя Machine . В нём могут быть приватные (private), публичные (public) и защищённые (protected) свойства:

Для наследования конструктор потомка вызывает родителя в своём контексте через apply . После чего может добавить свои переменные и методы:

В CoffeeMachine свойства, полученные от родителя, можно перезаписать своими. Но обычно требуется не заменить, а расширить метод родителя. Для этого он предварительно копируется в переменную:

Строку (*) можно упростить до parentProtected(args) , если метод родителя не использует this , а, например, привязан к var self = this :

Надо сказать, что способ наследования, описанный в этой главе, используется нечасто.

В следующих главах мы будем изучать прототипный подход, который обладает рядом преимуществ.

Но знать и понимать его необходимо, поскольку во многих существующих библиотеках классы написаны в функциональном стиле, и расширять/наследовать от них можно только так.

Задачи

Запускать только при включённой кофеварке

важность: 5

В коде CoffeeMachine сделайте так, чтобы метод run выводил ошибку, если кофеварка выключена.

Читайте также: