Наследует ли класс конструкторы своего суперкласса

Обновлено: 23.07.2024

Сначала я написал короткую статью для быстрого знакомства для тех, кто уже знал Java. Но потом решил, что всё больше людей будет начинать программировать на Kotlin без опыта на Java. Поэтому я стал расширять статью, беря готовые куски материала из разных источников. Например, я постарался подогнать часть своих примеров под примеры из книги "Head First Kotlin"

Наследование - важная тема в программировании. Наследование предотвращает дублирование кода. Общий код размещается в одном классе, а затем более конкретные специализированные классы наследуют этот класс. Если же код потребуется обновить, достаточно внести изменения в одном месте и эти изменения отразятся во всех классах, наследующих это поведение. Класс, содержащий общий код, называется суперклассом, а классы, наследующие от него, называются подклассами.

В Java классы открыты по умолчанию, а для того, чтобы запретить наследование от классов или переопределение их переменных и методов экземпляров, используется ключевое слово final. В Kotlin используется противоположный подход - можно наследовать от суперклассов и переопределять их свойства и функции только в том случае, если они снабжены префиксом open.

У нас уже был класс Cat, но теперь мы всё поменяем, чтобы показать принцип наследования.

Мы создадим суперкласс Animal, который будет содержать некоторые общие свойства и функции, которые будут наследоваться подклассами. В суперкласс добавим свойства image (имя файла с изображением животного), food (пища, которой питается животное, habitat (среда обитания), hunger (уровень голода животного), а также четыре функции: makeNoise() (животное издаёт свой характерный звук), eat() (что делает животное при обнаружении своего предпочтительного источника пищи), roam() (что делает животное, когда не ест и не спит), sleep() (животное засыпает).

Все животные выглядят по-разному, живут в разных местах и имеют разные гастрономические предпочтения. Это означает, что мы можем переопределить свойства, чтобы они по-разному инициализировались для каждого типа животного. Например, свойство habitat у класса Mouse будет инициализироваться значением «ground», а свойство food у Lion — значением «meat». Тоже самое с функциями. Каждый подкласс животного наследует функции от класса Animal. Какие же из этих функций следует переопределять? Львы рычат, волки воют, мыши пищат. Все животные издают разные звуки, это означает, что функция makeNoise() должна переопределяться в каждом подклассе животного.

Кроме того, можно сгруппировать некоторые виды классов, выстраивая иерархию. Например, волк и лисица относятся к семейству собачьих, и поэтому могут обладать общим поведением, которое можно абстрагировать в класс Canine. С другой стороны, лев, гепард и рысь относятся к семейству кошачьих, поэтому может быть полезно определить новый класс Feline.

Начнём с суперкласса Animal. Чтобы класс можно было использовать в качестве суперкласса, необходимо явно сообщить об этом компилятору. Для этого перед именем класса — и любым свойством и функцией, которые вы собираетесь переопределять, — ставится ключевое слово open. Тем самым вы сообщаете компилятору, что класс проектировался как суперкласс, и согласны с тем, что его свойства и функции, объявленные как открытые, будут переопределяться.

Мы объявили открытым сам класс, три свойства (кроме hunger) и три метода (кроме sleep).

Чтобы класс наследовал от другого класса, добавьте в заголовок класса двоеточие (:), за которым следует имя суперкласса. Класс становится подклассом и получает все свойства и функции того класса, от которого наследуется. В нашем случае класс Mouse должен наследовать от суперкласса Animal. Чтобы переопределить свойство, унаследованное от суперкласса, добавьте свойство в подкласс и поставьте перед ним ключевое слово override.

Переопределяем свойства image, food и habitat, унаследованные классом Mouse от суперкласса Animal, чтобы они инициализировались значениями, специфическими для Mouse.

Animal() после двоеточия (:) — вызов конструктора Animal. Он обеспечивает выполнение всего кода инициализации Animal — например, присваивание значений свойствам. Вызов конструктора суперкласса обязателен: если у суперкласса есть первичный конструктор, вы должны вызвать его в заголовке подкласса, иначе код не будет компилироваться. И даже если вы явно не добавили конструктор в свой суперкласс, помните, что компилятор автоматически создаёт пустой конструктор при компиляции кода.

Если конструктор суперкласса получает параметры, значения этих параметров должны передаваться при вызове конструктора.

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

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

Если свойство в суперклассе было определено с ключевым словом val, в подклассе оно может быть переопределено как свойство var. Для этого просто переопределите свойство и объявите его с ключевым словом var. Учтите, что замена работает только в одном направлении: при попытке переопределить свойство var с ключевым словом val компилятор откажется компилировать ваш код.

Функции переопределяются по аналогии со свойствами - добавляется в подкласс с префиксом override. При переопределении функций необходимо соблюдать два правила:

  1. Параметры функции в подклассе должны соответствовать параметрам функции в суперклассе. Если функция в суперклассе получает три аргумента Int, то переопределённая функция в подклассе тоже должна получать три аргумента Int; в противном случае код не будет компилироваться.
  2. Возвращаемые типы функций должны быть совместимыми. Какой бы возвращаемый тип ни был объявлен функцией суперкласса, переопределяющая функция должна возвращать либо тот же тип, либо тип подкласса. Тип подкласса гарантированно делает все, что объявлено в его суперклассе, поэтому подкласс можно безопасно вернуть в любой ситуации, где ожидается суперкласс

Ранее говорилось, чтобы переопределить функцию или свойство, необходимо объявить их открытыми в суперклассе. При этом функция или свойство остаются открытыми в каждом из подклассов, даже если они были переопределены, так что вам не придётся объявлять их открытыми ниже по дереву. Если вы хотите запретить возможность переопределения функции или свойства ниже в иерархии классов, снабдите их префиксом final.

Добавим новый класс, которые послужит промежуточным классом для некоторых животных - класс Canine (класс собачьих) и класс Wolf (волк), который будет наследоваться уже от него.

Хотя в классе Wolf мы видим только две функции, на самом деле класс содержит четыре функции. И мы можем вызвать любую из них.

Функция sleep наследуется от Animal, roam() от Canine (которая в свою очередь наследуется от Animal) и две функции переопределены в самом классе.

При вызове функции по ссылке на объект будет вызвана самая конкретная версия функции для этого типа объекта: то есть та, которая находится ниже всего в дереве наследования. Система сначала ищет функцию в классе Wolf. Если функция будет найдена в этом классе, то она выполняется. Но если функция не определена в классе Wolf, система идёт вверх по дереву наследования до класса Canine. Если функция определена в этом классе, система выполняет ее, а если нет, то поиск вверх по дереву продолжается. Система продолжает идти вверх по иерархии классов, пока не найдёт совпадение для функции.

Вот почему при вызове функции makeNoise() мы получим строку "Воет! У-у-у-у!", а не "Животное издаёт звук".

Наследование обеспечивает наличие функций и свойств во всех подклассах, определённых в суперклассе.

Когда вы определяете супертип для группы классов, вы можете использовать любой подкласс вместо суперкласса, от которого он наследуется. Можно написать следующее:

При вызове функции eat() будет вызвана версия класса Wolf, так как система знает, что по ссылке хранится объект Wolf.

Это даёт возможность создать массив из Animal на основе разных типов животных, но при этом каждый элемент массива будет выполнять свои функции.

Похожим образом поведёт себя функция, объявленная в другом классе, которая будет использовать в параметре Animal.

Айболит может сделать любому животному укол, так как волк и мышка являются разновидностями Animal.

Возможность использования объектов одного типа в месте, в котором явно обозначен другой тип, называется полиморфизмом. По сути, речь идёт о возможности предоставлять разные реализации функций, которые были унаследованы от другого класса.

Стоит заметить, что мы можем создать экземпляры класса Wolf и Mouse, но не должны иметь возможность создать экземпляр класса Animal, так такого абстрактного животного не существует. Для этой цели существуют абстрактные классы.

Наследование является важной особенностью объектно-ориентированного языка программирования. Наследование классов в Kotlin позволяет наследовать функцию существующего класса (или базового, или родительского класса) новому классу (или производному классу, или дочернему классу).

Основной класс называется суперклассом (или родительским классом), а класс, который наследует суперкласс, называется подклассом (или дочерним классом). Подкласс содержит черты суперкласса, а также свои собственные.

Концепция наследования допускается, когда два или более класса имеют одинаковые свойства. Это позволяет повторно использовать код. Производный класс имеет только один базовый класс, но может иметь несколько интерфейсов, тогда как базовый класс может иметь один или несколько производных классов.

В Kotlin производный класс наследует базовый класс, используя: оператор в заголовке класса (после имени производного класса или конструктора)

Предположим, что у нас есть два разных класса «Programmer» и «Salesman», имеющие общие свойства «name», «age» и «salary», а также свои собственные отдельные функции doProgram() и fieldWork(). Функция наследования позволяет нам наследовать (Employee), содержащий общие функции.

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

Например, класс Example неявно наследуется от Any.

Ключевое слово open

Поскольку классы Kotlin по умолчанию являются окончательными, их нельзя просто наследовать. Мы используем ключевое слово open перед классом, чтобы наследовать класс и сделать его неокончательным.

Наследование полей от класса

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

Наследование методов от класса Kotlin

Пример наследования Kotlin

Здесь мы объявляем класс Employee суперклассом, а Programmer и Salesman — их подклассами. Подклассы наследуют имя свойств, возраст и зарплату, а также подклассы содержат свои собственные функции, такие как doProgram() и fieldWork().

Первичный конструктор в наследовании

Если базовый и производный класс имеют первичный конструктор, в этом случае параметры инициализируются в первичном конструкторе базового класса. В приведенном выше примере наследования все классы содержат три параметра «имя», «возраст» и «зарплата», и все эти параметры инициализируются в первичном конструкторе базового класса.

Когда базовый и производный класс содержат разное количество параметров в своем основном конструкторе, тогда параметры базового класса инициализируются из объекта производного класса.

Когда создается объект производного класса, он сначала вызывает свой суперкласс и выполняет блок инициализации базового класса, а затем свой собственный.

Вторичный конструктор

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

В приведенном выше примере, когда создается объект класса Child, он вызывает свой конструктор и инициализирует его параметры значениями «Ashu», «101» и «Developer». В то же время конструктор дочернего класса вызывает свой конструктор суперкласса, используя ключевое слово super со значениями имени и идентификатора. Из-за наличия ключевого слова super тело конструктора суперкласса выполняется первым и возвращается к конструктору дочернего класса.

Переопределение метода Kotlin

Переопределение метода означает предоставление конкретной реализации метода super (родительского) класса в его подкласс (дочерний класс).

Другими словами, когда подкласс переопределяет или изменяет метод своего суперкласса на подкласс, это называется переопределением метода. Переопределение метода возможно только при наследовании.

Правила переопределения методов в Kotlin:

  • Родительский класс и его метод или свойство, которые должны быть переопределены, должны быть открытыми (неокончательными).
  • Имя метода базового класса и производного класса должно совпадать.
  • Метод должен иметь тот же параметр, что и в базовом классе.

Пример без переопределения

В приведенном выше примере программы без переопределения метода базового класса мы обнаружили, что оба производных класса Parrot и Duck выполняют одну и ту же общую операцию. Чтобы решить эту проблему, мы используем концепцию переопределения метода.

Пример переопределения метода Kotlin

В этом примере метод fly() родительского класса Bird переопределяется в его подклассах Parrot и Duck. Чтобы переопределить метод родительского класса, родительский класс и его метод, который будет переопределен, должны быть объявлены как открытые. В то же время метод, который переопределяется в дочернем классе, должен предваряться ключевым словом override.

Переопределение свойств класса Kotlin

Свойство суперкласса также может быть переопределено в его подклассе аналогично методу. Свойство цвета класса Bird переопределяется и модифицируется в его подклассах Parrot и Duck.

Мы можем переопределить свойство val свойством var в наследовании, но наоборот неверно.

Реализация суперкласса Kotlin

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

Реализация нескольких классов Kotlin

В Kotlin производный класс использует имя супертипа в угловых скобках, например, gsuper, когда он реализует одно и то же имя функции, предоставленное в нескольких классах.

Например, производный класс Parrotext является продолжением своего суперкласса Bird и реализует интерфейс Duck, содержащий ту же функцию fly(). Чтобы вызвать конкретный метод каждого класса и интерфейса, мы должны указать имя супертипа в угловых скобках как super.fly() и super.fly() для каждого метода.

Наследование Java дает возможность одному классу наследовать свойства другого класса. Также называется расширением класса .

Когда один класс наследуется от другого класса, эти два класса принимают определенные роли. Подкласс расширяет суперкласс. Или подкласс наследует от суперкласса. Подкласс – это специализация суперкласса, а суперкласс – это обобщение одного или нескольких подклассов.

Как метод повторного использования кода

Наследование может быть эффективным методом для обмена кодом между классами, которые имеют некоторые общие черты, но позволяя классам иметь некоторые части, которые отличаются.

Вот диаграмма, иллюстрирующая класс с именем Vehicle, который имеет два подкласса, называемые Car и Truck.

объяснение наследования

Класс Vehicle является суперклассом легковых и грузовых автомобилей. Автомобиль и Грузовик – подклассы Автомобиля. Класс Vehicle может содержать те поля и методы, которые нужны всем транспортным средствам (например, номерной знак, владелец и т. д.), Тогда как Car и Truck могут содержать поля и методы, специфичные для легковых и грузовых автомобилей.

Примечание. Некоторые люди утверждают, что наследование – это способ классификации ваших классов в зависимости от того, чем они являются. Автомобиль – это Автомобиль. Грузовик – транспортное средство. Однако на практике это не то, как вы определяете, какие суперклассы и подклассы должны иметь ваше приложение. Обычно это определяется тем, как вам нужно работать с ними в приложении.

Например, вам нужно ссылаться на объекты Car и Truck как объекты Vehicle? Вам нужно обрабатывать объекты Car и Truck одинаково? Тогда имеет смысл иметь общий суперкласс Vehicle для двух классов. Если вы никогда не обрабатываете объекты Car и Truck одним и тем же способом, нет смысла иметь для них общий суперкласс, кроме, возможно, совместного использования кода между ними (чтобы избежать написания дублирующего кода).

Классовые иерархии

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

Основы

Когда класс наследует от суперкласса, он наследует части методов и полей суперкласса. Подкласс также может переопределять (переопределять) унаследованные методы. Поля не могут быть переопределены, но могут быть «затенены» в подклассах. Как все это работает, рассказывается далее в этом тексте.

Что унаследовано?

Когда подкласс расширяет суперкласс в Java, он наследует все защищенные и открытые поля и методы, которые становятся его частью, как если бы он объявил их сам. Защищенные и открытые поля можно вызывать и ссылаться так же, как на методы, объявленные непосредственно в нем.

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

Конструкторы не наследуются подклассами, но конструктор подкласса должен вызывать конструктор в суперклассе.

Единичное наследование

Механизм наследования позволяет наследовать класс только от одного суперкласса (единичное наследование). В некоторых языках программирования, таких как C ++, подкласс может наследоваться от нескольких суперклассов (множественное).

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

Объявление

Объявляется с использованием ключевого слова extends:

Класс Car в этом примере расширяет класс Vehicle, то есть Car наследуется от Vehicle. Поскольку Car расширяет Vehicle, защищенное поле licensePlate из Vehicle наследуется Car. Когда licensePlate наследуется, оно становится доступным внутри экземпляра Car.

В поле licensePlate на самом деле не ссылаются из класса Car в приведенном выше коде, но можно, если мы захотим:

Ссылка происходит внутри метода getLicensePlate(). Во многих случаях имело бы смысл поместить этот метод в класс Vehicle, где находится поле licensePlate.

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

Можно ссылаться на подкласс как на экземпляр одного из его суперклассов. Например, используя определения класса из примера в предыдущем разделе, можно ссылаться на экземпляр класса Car как на экземпляр класса Vehicle. Так как Car расширяет (наследует) Vehicle, он также называется Vehicle.

Вот пример кода Java:

  1. Сначала создается экземпляр автомобиля.
  2. Экземпляр Car присваивается переменной типа Vehicle.
  3. Теперь переменная Vehicle (ссылка) указывает на экземпляр Car. Это возможно, потому что Car наследуется от Vehicle.

Как видите, можно использовать экземпляр некоторого подкласса, как если бы он был экземпляром его суперкласса. Таким образом, вам не нужно точно знать, к какому подклассу относится объект. Например, вы можете рассматривать экземпляры Грузовика и Автомобиля как экземпляры Транспортного средства.

Процесс ссылки на объект класса как тип, отличный от самого класса, называется приведением типа . Вы бросаете объект из одного типа в другой.

Upcasting и Downcasting

Вы всегда можете привести объект подкласса к одному из его суперклассов, либо из типа суперкласса к типу подкласса, но только если объект действительно является экземпляром этого подкласса (или экземпляром подкласса этого подкласса). Таким образом, этот пример downcasting действителен:

Однако следующий приведенный ниже пример недопустим. Компилятор примет его, но во время выполнения, выдаст исключение ClassCastException.

Объект «Грузовик» может быть передан объекту «Автомобиль», но позже он не может быть передан объекту «Автомобиль». Это приведет к исключению ClassCastException.

Переопределяющие методы

В подклассе вы можете переопределить методы, определенные в суперклассе:

Обратите внимание, как и класс Vehicle, и класс Car определяют метод setLicensePlate(). Теперь каждый раз, когда setLicensePlate() вызывается для объекта Car, вызывается метод, определенный в классе Car. Метод в суперклассе игнорируется.

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

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

Аннотация @override

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

Для этого и нужна аннотация @ override. Вы размещаете ее над методом, который переопределяет метод в суперклассе:

Вызов методов суперкласса

Если вы переопределяете метод в подклассе, но по-прежнему должны вызывать метод, определенный в суперклассе, используйте ссылку super, например:

В приведенном выше примере кода метод setLicensePlate() в классе Car вызывает метод setLicensePlate() в классе Vehicle.

Вы можете вызывать реализации суперкласса из любого метода в подклассе, как описано выше. Он не должен быть из самого переопределенного метода. Например, вы могли бы также вызвать super.setLicensePlate() из метода в классе Car с именем updateLicensePlate(), который не переопределяет метод setLicensePlate().

Пример инструкции

Java содержит инструкцию с именем instanceof. Она может определить, является ли данный объект экземпляром некоторого класса:

После выполнения этого кода переменная isCar будет содержать значение true.

Инструкция instanceof также может использоваться для определения того, является ли объект экземпляром суперкласса своего класса. Вот пример, который проверяет, является ли объект Car экземпляром Vehicle:

Предполагая, что класс Car расширяет (наследует от) класс Vehicle, переменная isVehicle будет содержать значение true после выполнения этого кода. Объект Car также является объектом Vehicle, поскольку Car является подклассом Vehicle.

Как видите, инструкция instanceof может использоваться для изучения иерархии наследования. Тип переменной, используемый с ней, не влияет на ее результат. Посмотрите на этот пример:

Несмотря на то, что переменная транспортного средства имеет тип Vehicle, объект, на который она в конечном итоге указывает в этом примере, является объектом Car. Поэтому экземпляр транспортного средства автомобиля будет оценен как истинный.

Вот тот же пример, но с использованием объекта Truck вместо объекта Car:

После выполнения этого кода isCar будет содержать значение false. Объект Truck не является объектом Car.

Как наследуются

Как упоминалось ранее, в Java поля не могут быть переопределены в подклассе. Если вы определяете поле в подклассе с тем же именем, что и в суперклассе, поле в подклассе будет скрывать поле в суперклассе. Если подкласс попытается получить доступ к полю, он получит доступ к полю в подклассе.

Однако, если подкласс вызывает метод в суперклассе, который обращается к полю с тем же именем, что и в подклассе, это поле в суперклассе, к которому осуществляется доступ.

Вот пример, который иллюстрирует, как поля в подклассах скрывают поля в суперклассах:

Обратите внимание, как для обоих классов определено поле licensePlate.

И класс Vehicle, и класс Car имеют методы setLicensePlate() и getLicensePlate(). Методы в классе Car вызывают соответствующие методы в классе Vehicle. В результате оба набора методов получают доступ к полю licensePlate в классе Vehicle.

Однако метод updateLicensePlate() в классе Car напрямую обращается к полю licensePlate. Таким образом, он получает доступ к полю licensePlate класса Car. Следовательно, вы не получите тот же результат, если вызовете setLicensePlate(), как при вызове метода updateLicense().

Посмотрите на следующие строки кода:

Этот код распечатает текст 123.

Метод updateLicensePlate() устанавливает значение номерного знака в поле licensePlate в классе Car. Однако метод getLicensePlate() возвращает значение поля licensePlate в классе Vehicle. Следовательно, значение 123, которое устанавливается как значение для поля licensePlate в классе Vehicle с помощью метода setLicensePlate(), является тем, что выводится на печать.

Конструкторы

Механизм наследования не включает конструкторы. Другими словами, конструкторы суперкласса не наследуются подклассами. Подклассы могут по-прежнему вызывать конструкторы в суперклассе, используя конструкцию super().

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

Обратите внимание на вызов super() внутри конструктора Car. Этот вызов super() выполняет конструктор в классе Vehicle.

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

Если класс не имеет какого-либо явного конструктора, компилятор вставляет неявный без аргументов. Таким образом, класс всегда имеет конструктор.

Поэтому следующая версия транспортного средства эквивалентна версии, показанной чуть выше:

Если конструктор явно не вызывает конструктор в суперклассе, компилятор вставляет неявный вызов конструктора no-arg в суперклассе. Это означает, что следующая версия класса Car фактически эквивалентна версии, показанной ранее:

Фактически, поскольку конструктор теперь пуст, мы могли бы опустить его, и компилятор вставил бы его и неявный вызов конструктора no-arg в суперклассе. Вот как тогда будут выглядеть два класса:

Даже если в этих двух классах не объявлено ни одного конструктора, они оба получают конструктор без аргументов, который в классе Car будет вызывать конструктор без аргументов в классе Vehicle.

Если бы класс Vehicle не имел конструктора без аргументов, но имел другой, который принимает параметры, компилятор жаловался бы. Класс Car затем должен был бы объявить конструктор, а внутри него вызвать конструктор в классе Vehicle.

Вложенные классы

Те же правила наследования применяются к вложенным классам. Если объявлены закрытыми, не наследуются. Вложенные классы с модификатором доступа по умолчанию (пакет) доступны только для подклассов, если подкласс находится в том же пакете, что и суперкласс. С модификатором защищенного или открытого доступа всегда наследуются подклассами.

Обратите внимание, как можно создать экземпляр вложенного класса MyNestedClass, который определен в суперклассе(MyClass) посредством ссылки на подкласс(MySubclass).

Финальные классы

Класс может быть объявлен окончательным(final):

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

Абстрактные классы

Класс может быть объявлен абстрактным, который не содержит полную реализацию того, что должен делать. Таким образом, это не может быть реализовано. Другими словами, вы не можете создавать объекты абстрактного класса.

Они предназначены для расширения / создания полной реализации. Таким образом, вполне возможно расширить абстрактный класс. Правила наследования такие же для них, как и для неабстрактных классов.

В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.

Что такое наследование?

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

Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.

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

В этом примере, метод turn_on() и переменная serial_number не были объявлены или определены в подклассе Computer . Однако их можно использовать, поскольку они унаследованы от базового класса.

Важное примечание: приватные переменные и методы не могут быть унаследованы.

Типы наследования

В C ++ есть несколько типов наследования:

  • публичный ( public )- публичные ( public ) и защищенные ( protected ) данные наследуются без изменения уровня доступа к ним;
  • защищенный ( protected ) — все унаследованные данные становятся защищенными;
  • приватный ( private ) — все унаследованные данные становятся приватными.

Для базового класса Device , уровень доступа к данным не изменяется, но поскольку производный класс Computer наследует данные как приватные, данные становятся приватными для класса Computer .

Класс Computer теперь использует метод turn_on() как и любой приватный метод: turn_on() может быть вызван изнутри класса, но попытка вызвать его напрямую из main приведет к ошибке во время компиляции. Для базового класса Device , метод turn_on() остался публичным, и может быть вызван из main .

Конструкторы и деструкторы

В C ++ конструкторы и деструкторы не наследуются. Однако они вызываются, когда дочерний класс инициализирует свой объект. Конструкторы вызываются один за другим иерархически, начиная с базового класса и заканчивая последним производным классом. Деструкторы вызываются в обратном порядке.

Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.

Конструкторы: Device -> Computer -> Laptop .
Деструкторы: Laptop -> Computer -> Device .

Множественное наследование

Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.

Проблематика множественного наследования

Множественное наследование требует тщательного проектирования, так как может привести к непредвиденным последствиям. Большинство таких последствий вызваны неоднозначностью в наследовании. В данном примере Laptop наследует метод turn_on() от обоих родителей и неясно какой метод должен быть вызван.

Несмотря на то, что приватные данные не наследуются, разрешить неоднозначное наследование изменением уровня доступа к данным на приватный невозможно. При компиляции, сначала происходит поиск метода или переменной, а уже после — проверка уровня доступа к ним.

Проблема ромба

Проблема ромба (Diamond problem)- классическая проблема в языках, которые поддерживают возможность множественного наследования. Эта проблема возникает когда классы B и C наследуют A , а класс D наследует B и C .

К примеру, классы A , B и C определяют метод print_letter() . Если print_letter() будет вызываться классом D , неясно какой метод должен быть вызван — метод класса A , B или C . Разные языки по-разному подходят к решению ромбовидной проблем. В C ++ решение проблемы оставлено на усмотрение программиста.

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

  • вызвать метод конкретного суперкласса;
  • обратиться к объекту подкласса как к объекту определенного суперкласса;
  • переопределить проблематичный метод в последнем дочернем классе (в коде — turn_on() в подклассе Laptop ).

Если метод turn_on() не был переопределен в Laptop, вызов Laptop_instance.turn_on() , приведет к ошибке при компиляции. Объект Laptop может получить доступ к двум определениям метода turn_on() одновременно: Device:Computer:Laptop.turn_on() и Device:Monitor:Laptop.turn_on() .

Проблема ромба: Конструкторы и деструкторы

Поскольку в С++ при инициализации объекта дочернего класса вызываются конструкторы всех родительских классов, возникает и другая проблема: конструктор базового класса Device будет вызван дважды.

Виртуальное наследование

Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.

Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device <>; ).

Абстрактный класс

В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.

Интерфейс

С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).

Несмотря на то, что наследование — фундаментальный принцип ООП, его стоит использовать с осторожностью. Важно думать о том, что любой код который будет использоваться скорее всего будет изменен и может быть использован неочевидным для разработчика путем.

Наследование от реализованного или частично реализованного класса

Если наследование происходит не от интерфейса (чистого абстрактного класса в контексте С++), а от класса в котором присутствуют какие-либо реализации, стоит учитывать то, что класс наследник связан с родительским классом наиболее тесной из возможных связью. Большинство изменений в классе родителя могут затронуть наследника что может привести к непредвиденному поведению. Такие изменения в поведении наследника не всегда очевидны — ошибка может возникнуть в уже оттестированом и рабочем коде. Данная ситуация усугубляется наличием сложной иерархии классов. Всегда стоит помнить о том, что код может изменяться не только человеком который его написал, и пути наследования очевидные для автора могут быть не учтены его коллегами.

В противовес этому стоит заметить что наследование от частично реализованных классов имеет неоспоримое преимущество. Библиотеки и фреймворки зачастую работают следующим образом: они предоставляют пользователю абстрактный класс с несколькими виртуальными и множеством реализованных методов. Таким образом, наибольшее количество работы уже проделано — сложная логика уже написана, а пользователю остается только кастомизировать готовое решение под свои нужды.

Интерфейс

Наследование от интерфейса (чистого абстрактного класса) преподносит наследование как возможность структурирования кода и защиту пользователя. Так как интерфейс описывает какую работу будет выполнять класс-реализация, но не описывает как именно, любой пользователь интерфейса огражден от изменений в классе который реализует этот интерфейс.

Интерфейс: Пример использования

Прежде всего стоит заметить, что пример тесно связан с понятием полиморфизма, но будет рассмотрен в контексте наследования от чистого абстрактного класса.

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

Отсутствие однозначности касательно форматирования конфигурационного файла не тормозит процесс разработки основной программы. Два разработчика могут работать параллельно — один над бизнес логикой, а другой над парсером. Поскольку они взаимодействуют через этот интерфейс, каждый из них может работать независимо. Данный подход облегчает покрытие кода юнит тестами, так как необходимые тесты могут быть написаны с использованием мока (mock) для этого интерфейса.

Также, при изменении формата конфигурационного файла, бизнес логика приложения не затрагивается. Единственное чего требует полный переход от одного форматирования к другому — написания новой реализации уже существующего абстрактного класса (класса-парсера). В дальнейшем, возврат к изначальному формату файла требует минимальной работы — подмены одного уже существующего парсера другим.

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

Недавно в офисе Хабра я хотел прочитать своим коллегам небольшой доклад об объектной ориентации и наследовании классов в JavaScript.

Дело в том, что в свое время я был в полном восторге, научившись создавать свои собственные объекты и выстраивать цепочки наследования, и решил, что называется, поделиться с другими своими находками и наблюдениями. (=

Признаюсь, что в процессе подготовки текстов семинара некоторые вещи я открыл для себя заново и был вновь удивлен теми возможностями, которые JavaScript имеет в своем распоряжении, и которые становятся доступными разработчику лишь при пристальном разглядывании и экспериментировании языком.

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

  1. ООП в Java Script (1/5): Объекты
  2. ООП в Java Script (2/5): Классы
  3. ООП в Java Script (3/5): Свойства и методы класса
  4. ООП в Java Script (4/5): Наследование классов
  5. ООП в Java Script (5/5): Полезные ссылки

ООП в Java Script (1/5): Объекты

Все в JavaScript, на самом деле, является объектом. Массив — это объект. Функция — это объект. Объект — тоже объект. Так что такое объект? Объект — это коллекция свойств. Каждое свойство представляет собой пару имя-значение. Имя свойства — это строка, а значение свойства — строка, число, булево значение, или объект (включая массив и функцию).

Когда мы определяем какую-то переменную, например:


мы, в действительности, неявням образом задаем свойство какого-то объекта. В данном случае, таким объектом будет глобальный объект window:


Более того, это свойство window.s само по себе является объектом, т.к. в нем уже изначально определена своя коллекция свойств:


При всем при том, что это, на первый взгляд, обычный строковый литерал!

Если значение свойства — функция, мы можем назвать это свойство методом объекта. Чтобы вызвать метод объекта достаточно дописать после его имени две круглые скобки (). Когда метод объекта выполняется, переменная this внутри этой функции ссылается на сам объект. С помощью ключевого слова this метод объекта получает доступ ко всем остальным свойствам и методам объета.


Объект создается с помощью функции-конструктора, инициализирующей объект, и ключевого слова new . Функция-конструктор предоставляет те же возможности, что и класс в других языках программирования: а именно, описывает шаблон, по которому будут создаваться объекты (экземпляры) класса. В основе такого шаблона лежит перечисление свойств и методов, которыми будет обладать объект, созданный на основе данного класса. Для всех встроенных типов данных в JavaScript существуют встроенные функции-конструткоры.

Например, когда мы объявляем строковую переменую:


мы неявным образом вызываем встроенную функцию-конструтор:


и тем самым создаем объект (экземпляр) класса String .

Это же утверждение верно и для всех остальных типов данных JavaScript:


У всех этих объектов сразу же после создания определены все свойства и методы, описанные в их функциях-конструкторах (классах):


На самом деле, интерпретатор JavaScript действует несколько хитрее, чем может показаться из предыдущего примера. Так, несмотря на то, что следующий код показывает равенство двух переменных (объектов класса String ):


при попытке определить новый пользовательский метод для str1 мы получим ошибку:


При этом, для str2 все сработает, как мы и ожидаем:


Это ограничение, наложенное JavaScript на переменные (объекты), созданные через строковые, числовые и булевы литералы, тем не менее, не распространяется на объекты, созданные через литералы функции, массива или объекта. Т.е. переменным (объектам), содержащим в себе функцию, или массив, или объект можно напрямую присваивать пользовательские свойства и методы:


Здесь мы наглядно убеждаемся, что функция f, созданная как метод глобального объекта window, сама оказывается объектом, у которого могут быть свои собственные свойства и методы!

ООП в Java Script (2/5): Классы

Итак, класс — это шаблон, описывающий свойства и методы, которыми будет обладать любой объект, созданный на основе этого класса. Чтобы создать свой собственный класс в JavaScript, мы должны написать функцию-конструктор:


А чтобы создать объект этого нового класса, мы должны вызвать его как обычную функцию, используя при этом ключевое слово new. При этом ключевое слово this внутри функции-конструтора теперь будет указывать на вновь созданный объект:


Если попытыться переменной o просто присвоить вызов функции Class() — без ключевого слова new, то никакого объекта создано не будет:


При создании функции, JavaScript автоматически создает для нее пустое свойство .prototype . Любые свойства и методы, записанные в .prototype функции-конструтора станут доступными как свойства и методы объектов, созданных на основе этой функции. Это является основой для описания шаблона (класса), по которому и будут создаваться объеты.


Теперь мы можем вызывать этот метод, как метод самого объекта:


При вызове свойства объекта, оно ищется сначала в самом объекте, и если его там не оказывается, то интепретатор смотрит в .prototype функции-конструтора, содавшей объект.

Так, при создании объекта, в нем уже существует свойство .constructor , которое указывает на функцию-конструктор, создавшую этот объект:


Заметим, что мы не определяли такого свойства в самом объекте. Интерпретатор, не найдя свойство .constructor в объекте, берет его из .prototype функции-конструктора, создавшей объект. Проверим:


Следует обратить внимание, что .prototype существует только для функции-конструктора, но не для самого объекта, созданного на его основе:


Доступ к .prototype функции-конструктора существует у всех объектов, в том числе и у объектов, встроенных в JavaScript, таких как строки, числа и т.п. Причем тут уже нет никаких ограничений в создании собсвенных свойств и методов (мы видели эти ограничения при попытке прямого присвоения свойств и методов строковой переменной — объекту, созданному через строковый литерал):


Задать новое свойство или метод для встроенных типов объектов можно и напрямую — через встроенную функцию-конструтор этих объектов:


Кстати, мы в очередной раз подтвердили утверждение о том, что все в JavaScript есть объект (=

ООП в Java Script (3/5): Свойства и методы класса

Свойства и методы класса (члены класса) могут быть открытыми (public), закрытыми (private), привилегированными (privileged) и статическими (static).

Открытые (public) члены

Открытыми называют такие свойства и методы, которые могут быть напрямую прочитаны, изменены, удалены или добавлены любым кодом, находящимся за пределами самого объекта.
Открытые свойства задаются с помощью ключевого слова .this внутри функции-конструктора:


Открытые методы задаются с помощью .prototype функции-конструтора:


Присваивая объекту obj метод .method , мы не изменяем одноименный метод в .prototype функции-конструтора, а лишь закрываем его от интерпретатора, создавая в нашем объекте новое свойство с тем же именем. Т.е. все вновь создаваемые объекты будут по-прежнему обладать стандартным методом из .prototype .

Мы можем позволить объекту вновь видеть и пользоваться методом из .prototype . Для этого нужно просто удалить свойство .method самого объекта:


Свойства и методы, заданные через .prototype функции-конструктора, не копируются во вновь создаваемые объекты. Все объекты данного класса пользуются ссылкой на одни и те же свойства и методы. Одновременно, открытые члены мы можем определять в любой точке программы, в том числе даже и после создания объекта (экземпляра) класса.

Закрытые (private) члены

Закрытые свойства и методы недоступны напрямую извне объекта. Они описываются прямо в функции-конструкторе класса и создаются при инициализации объекта. Такими свойствами обладают переменные, переданные в качестве параметров в функцию-конструтор, переменные, объявленные с помощью ключегого слова var, а также функции, объявленные как локальные внутри функции-конструтора.


Свойствa secret , count и метод counter создаются в объекте при его инициализации. Они называются закрытыми, потому что к ним нет доступа как у кода извне объекта, так и у открытых методов самого объекта. Чтобы понять, как можно использовать эти закрытые свойства, нужно обратиться к привилегированным методам.

Привилегированные (privileged) методы

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

Привилегированный метод определяется в конструкторе с помощью ключевого слова this:


.tellSecret и есть привилегированный метод. Он возвращает закрытое свойство secret при первых трех вызовах, а при всех последующих начинает возвращать null . Каждый раз .tellSecret вызывает закрытый метод counter , который сам обладает доступом к закрытым свойствам объекта. Любой код имеет доступ к методу .tellSecret , но это не дает прямого доступа к закрытым членам объекта.

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

Статические (static) члены

Статические свойства и методы — это свойства и методы, привязанные к самой функции-конструтору (к самому классу). Поэтому их еще называют свойствами и методами класса. Они доступны любому коду как внутри, так и за пределами объекта:

Замыкание (closure)

Закрытые и привилегированные методы возможны в JavaScript благодаря тому, что называется замыканием (closure). Замыкание — это функция, плюс все те лексические переменные из охватывающего контекста, которые она использует. Когда мы используем оператор function , мы всегда создаем не функцию, а именно замыкание. Замыкание 'помнит' значения всех переменых, которые существовали в контексте, создающем это замыкание, даже когда функция используется уже вне создавшего ее контекста.


Теперь, если мы посмотрим на переменную f , мы увидим, что это обычная функция, в теле которой заключен параметр closureParam , который неопределен нигде в окружающем f контектсе и по идее должен выдавать ошибку:


Однако ошибки не будет, функция function() благодаря эффекту замыкания помнит closureParam из контекста, породившего ее:


Если вспомнить описанный выше привилегированный метод .tellSecret , то теперь можно понять, как он работает. Метод помнит как закрытую функцию count() , так и закрытое свойство secret , объявленные в создающем .tellSecret контексте. При этом, когда внутри .tellSecret вызывается count() , эта последняя функция, в свою очередь, помнит использующуюся в ее теле переменую count .

ООП в Java Script (4/5): Наследование классов

Основные принципы наследования классов:

  1. Подкласс всегда наследует все свойства и методы, определенные в его надклассе.
  2. Подкласс может переопределять наследуемые свойства и методы, а также создавать новые — и это никак не должно отражаться на одноименных свойствах и методах надкласса.
  3. Подкласс должен иметь возможность вызывать родные методы надкласса даже в том случае, если переопределяет их.
  4. Объекты подкласса должны инициализироваться только в момент своего создания.

Зная о таком поведении JavaScript, попробуем создать наследование двух классов:


Видим, что подкласс унаследовал метод .method своего надкласса (выполняет его, как свой собственный). Как это происходит? Сначала интерпретатор ищет метод .method в самом объекте objSub и естественно не находит его там. Далее, интерпретатор обращается к ClassSub.prototype и ищет .method среди свойств этого объекта. Опять же — ничего не находит: мы нигде не задавали ничего похожего на ClassSub.prototype.method = function()<> . Но ведь сам объект ClassSub.prototype создан из функции-конструтора Class() . Поэтому, не найдя нужных свойств в самом ClassSub.prototype , интерпретатор обращается к .prototype функции-конструтора этого объекта. И уже здесь находит запрашиваемый метод: Class.prototype.method = function()<> .

Подтврдим это длинное рассуждение простым сравнением:


Подобная цепочка прототипов может быть сколь угодно длинной, но поиск интепретатора в любом случае закончится в тот момент, когда он доберется до объета, созданного (явно или неявно) из встроенного класса Object . Если в Object.prototype он и теперь не найдет запрашиваемого метода, то вернет ошибку. Класс Object лежит в самом верху любой возможной иерархии классов, создаваемых в JavaScript.

Теперь попробуем переопределить этот унаследованный метод, а заодно расширить подкласс собственным дополнительным методом. Одновременно проверим, что методы надкласса остались прежними (помним, что открытые методы и свойства можно добавлять даже после создания экземпляра класса):


Итак, пока все идет нормально. Мы переопределили метод .method в подклассе, и экземпляр подкласса стал выполнять именно его. Одновремнно, экземпляр надкласса сохранил свой прежний одноименный метод. Мы создали новый метод подкласса, который успешно работает в экземпляре подкласса. При этом этот новый метод не стал методом надкласса — экземпляр надкласса не видит его и выдает ошибку.

Все выглядит просто до тех пор, пока мы не попробуем написать более реалистичный код. Как правило, функция-конструктор не только определяет свойства объекта, но также, выполняет некоторые инициализирующие функции. Например, создадим класс Animal , в который в качестве параметра будет передаваться имя особи, и каждый новый экземпляр которого будет кричать при рождении (=

Теперь создадим подкласс Cat, экземпляры которого не кричат, а мяучат:


Запустив этот код, мы услышим не два крика (whoa!, meow!), а три! (whoa! ,whoa!, meow!) И понятно почему. Второй крик происходит в тот самый момент, когда мы делаем наследование Cat.prototype = new Animal(). Мы невольно создаем экземпляр класса Animal (и заставляем его кричать при рождении). Т.е. мы запускаем функцию-конструктор надкласса вхолостую еще до создания какого-либо экземпляра подкласса!

Кроме того, в подклассе мы полностью продублировали функцию-конструктор надкласса! Пока мы даже не видим, как иначе можно заставить подкласс присваивать объекту свойства, переданные через параметры функции-конструктора, и как по-другому заставить этот конструктор что-то делать вообще.

Решение проблемы холостого вызова функции-конструктора надкласса

Может попробовать не создавать экземпляр класса Animal , а просто указать на равенство прототипов двух классов? (ведь именно через свои прототипы они и связываются). Попробуем поменять эту строчку:


Запустив код, услышим два ожидаемых крика! Но это только кажется, что проблема решена. Попробуем сразу после создания экземпляра подкласса Cat создать еще один экземпляр надкласса Animal


Этот экземпляр кричит голосом класса Cat! Получилось, что мы перезаписали одноименный метод родительского класса. Все дело в том, что когда мы пишем Cat.prototype = Animal.prototype , мы передаем объекту Cat.prototype объект Animal.prototype по ссылке (как всегда происходит, когда переменной присваивается объект). Поэтому любые изменеия первого небезосновательно ведут к изменению второго. Кoгда же мы писали Cat.prototype = new Animal() , мы создавали в Cat.prototype новый объект. Меняя его свойства, мы никак не затрагивали свойства .prototype самой функции-конструктора объекта.

Попробуем реализовать наследование — без создания экземпляра родительского класса — несколько иначе. Попробуем просто скопировать в .prototype подкласса все свойства и методы из .prototype надкласса. Перепишем проблемную строчку следующим образом:


Запустим код и увидим, что третья особь уже больше не мяучит, т.е. метод родительского класса отсался прежним! Но хорошо ли мы поступили? На самом деле, мы не унаследовали свойства надкласса, а просто создали еще одну их копию. Если объектов подкласса будет много — то для каждого объекта будет создана собственная полная копия всех свойств надкласса. Более того, если попытаться поменять методы класса после создания объектов подкласса, то эти изменеия никак не отразятся на объектах подкласса! Такой код кажется очень негибким и громоздким.

Прибегнем к следующему хитрому приему: создадим в .prototype подкласса новый объект, обладающий ссылкой на .prototype надкласса, но при этом не запускающий функцию-конструктор надкласса. Перепишем сложную строчку еще раз:


Мы создали в Cat.prototype объект искусственного класса Empty . При создании этого объекта ничего не происходит, потому что функция-конструтор Empty() пуста. Любые присваивания в Cat.prototype будут касаться только изменения свойств самого объекта Cat.prototype и не будут затрагивать функцию-конструтор надкласса Animal . Если интепретатор не найдет требуемого метода ни в экземпляре класса Cat , ни в свойствах Cat.prototype , он обратится к функции-конструтору объекта Cat.prototype (== new Empty()) и начнет искать в Empty.prototype , который ссылается напрямую на нужный нам Animal.prototype

Решение проблемы дублирования функции-конструктора надкласса

Нам бы хотелось сделать примерно следующее:


Т.е. при инициализации каждого нового объекта подкласса Cat вызывать функцию-конструктор надкласса Animal в контексте объекта new Cat() . В принципе, наш код уже хорошо работает, но хотелось бы видеть его более универсальным — не привязанным к конкретным именам классов.

Сделаем одно лирическре отступление. Как мы помним, при создании любого объекта, у него образуется свойсво .constructor , берущееся из .prototype.constructor породившей его функции-конструктора. Однако, когда мы записали: Cat.prototype = new Empty() , мы создали в Cat.prototype новый объект. Если теперь попробовать обратиться к (new Cat()).constructor , интепретатор пойдет искать его в Cat.prototype.constructor , а значит в (new Empty().constructor) и найдет в результате это свойство в Empty.prototype.constructor ( == Animal.prototype.constructor) . Т.е. наше свойство .constructor указывает теперь на функцию-конструктор надкласса, а не подкласса! Мы исковеркали это свойство. Зная все это, прямо сейчас можно было бы записать:


и получить искомую универсальность кода, но скорее таким кодом мы внесем еще большую путанность, т.к. .constructor объекта должен указывать на функцию-конструткор подкласса, а не надкласса. Поэтому, посиупим так: на месте прошлой проблемной строки, в которой происходило наследование, запишем следующее:


Если нам теперь захочется в подклассе поменять наш метод не целиком, а только расширить его, мы можем легко сделать это так:


Наведем порядок в нашем коде и напишем универсальную функцию наследования. Запишем ее в .prototype встроенной функции-конструткора Function . Таким образом, мы создадим новый метод для всех возможных функций, в т.ч. и для наших пользовательских классов.

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