Наследование классов
Наследование классов – это способ расширения одного класса другим классом.
Таким образом, мы можем добавить новый функционал к уже существующему.
Ключевое слово «extends»
Допустим, у нас есть класс Animal :
class Animal < constructor(name) < this.speed = 0; this.name = name; >run(speed) < this.speed = speed; alert(`$бежит со скоростью $.`); > stop() < this.speed = 0; alert(`$стоит неподвижно.`); > > let animal = new Animal("Мой питомец");
Вот как мы можем представить объект animal и класс Animal графически:
…И мы хотели бы создать ещё один class Rabbit .
Поскольку кролики – это животные, класс Rabbit должен быть основан на Animal , и иметь доступ к методам животных, так чтобы кролики могли делать то, что могут делать «общие» животные.
Синтаксис для расширения другого класса следующий: class Child extends Parent .
Давайте создадим class Rabbit , который наследуется от Animal :
class Rabbit extends Animal < hide() < alert(`$прячется!`); > > let rabbit = new Rabbit("Белый кролик"); rabbit.run(5); // Белый кролик бежит со скоростью 5. rabbit.hide(); // Белый кролик прячется!
Объект класса Rabbit имеет доступ как к методам Rabbit , таким как rabbit.hide() , так и к методам Animal , таким как rabbit.run() .
Внутри ключевое слово extends работает по старой доброй механике прототипов. Оно устанавливает Rabbit.prototype.[[Prototype]] в Animal.prototype . Таким образом, если метода не оказалось в Rabbit.prototype , JavaScript берет его из Animal.prototype .
Например, чтобы найти метод rabbit.run , движок проверяет (снизу вверх на картинке):
- Объект rabbit (не имеет run ).
- Его прототип, то есть Rabbit.prototype (имеет hide , но не имеет run ).
- Его прототип, то есть (вследствие extends ) Animal.prototype , в котором, наконец, есть метод run .
Как мы помним из главы Встроенные прототипы, сам JavaScript использует наследование на прототипах для встроенных объектов. Например, Date.prototype.[[Prototype]] является Object.prototype , поэтому у дат есть универсальные методы объекта.
После extends разрешены любые выражения
Синтаксис создания класса допускает указывать после extends не только класс, но и любое выражение.
Пример вызова функции, которая генерирует родительский класс:
function f(phrase) < return class < sayHi() < alert(phrase); >>; > class User extends f("Привет") <> new User().sayHi(); // Привет
Здесь class User наследует от результата вызова f(«Привет») .
Это может быть полезно для продвинутых приёмов проектирования, где мы можем использовать функции для генерации классов в зависимости от многих условий и затем наследовать их.
Переопределение методов
Теперь давайте продвинемся дальше и переопределим метод. По умолчанию все методы, не указанные в классе Rabbit , берутся непосредственно «как есть» из класса Animal .
Но если мы укажем в Rabbit собственный метод, например stop() , то он будет использован вместо него:
class Rabbit extends Animal < stop() < // . теперь это будет использоваться для rabbit.stop() // вместо stop() из класса Animal >>
Впрочем, обычно мы не хотим полностью заменить родительский метод, а скорее хотим сделать новый на его основе, изменяя или расширяя его функциональность. Мы делаем что-то в нашем методе и вызываем родительский метод до/после или в процессе.
У классов есть ключевое слово «super» для таких случаев.
- super.method(. ) вызывает родительский метод.
- super(. ) для вызова родительского конструктора (работает только внутри нашего конструктора).
Пусть наш кролик автоматически прячется при остановке:
class Animal < constructor(name) < this.speed = 0; this.name = name; >run(speed) < this.speed = speed; alert(`$бежит со скоростью $.`); > stop() < this.speed = 0; alert(`$стоит неподвижно.`); > > class Rabbit extends Animal < hide() < alert(`$прячется!`); > stop() < super.stop(); // вызываем родительский метод stop this.hide(); // и затем hide >> let rabbit = new Rabbit("Белый кролик"); rabbit.run(5); // Белый кролик бежит со скоростью 5. rabbit.stop(); // Белый кролик стоит. Белый кролик прячется!
Теперь у класса Rabbit есть метод stop , который вызывает родительский super.stop() в процессе выполнения.
У стрелочных функций нет super
Как упоминалось в главе Повторяем стрелочные функции, стрелочные функции не имеют super .
При обращении к super стрелочной функции он берётся из внешней функции:
class Rabbit extends Animal < stop() < setTimeout(() =>super.stop(), 1000); // вызывает родительский stop после 1 секунды > >
В примере super в стрелочной функции тот же самый, что и в stop() , поэтому метод отрабатывает как и ожидается. Если бы мы указали здесь «обычную» функцию, была бы ошибка:
// Unexpected super setTimeout(function() < super.stop() >, 1000);
Переопределение конструктора
С конструкторами немного сложнее.
До сих пор у Rabbit не было своего конструктора.
Согласно спецификации, если класс расширяет другой класс и не имеет конструктора, то автоматически создаётся такой «пустой» конструктор:
class Rabbit extends Animal < // генерируется для классов-потомков, у которых нет своего конструктора constructor(. args) < super(. args); >>
Как мы видим, он просто вызывает конструктор родительского класса. Так будет происходить, пока мы не создадим собственный конструктор.
Давайте добавим конструктор для Rabbit . Он будет устанавливать earLength в дополнение к name :
class Animal < constructor(name) < this.speed = 0; this.name = name; >// . > class Rabbit extends Animal < constructor(name, earLength) < this.speed = 0; this.name = name; this.earLength = earLength; >// . > // Не работает! let rabbit = new Rabbit("Белый кролик", 10); // Error: this is not defined.
Упс! При создании кролика – ошибка! Что не так?
Если коротко, то:
- Конструкторы в наследуемых классах должны обязательно вызывать super(. ) , и (!) делать это перед использованием this ..
…Но почему? Что происходит? Это требование кажется довольно странным.
Конечно, всему есть своё объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит.
В JavaScript существует различие между «функцией-конструктором наследующего класса» и всеми остальными. В наследующем классе соответствующая функция-конструктор помечена специальным внутренним свойством [[ConstructorKind]]:»derived» .
Разница в следующем:
- Когда выполняется обычный конструктор, он создаёт пустой объект и присваивает его this .
- Когда запускается конструктор унаследованного класса, он этого не делает. Вместо этого он ждёт, что это сделает конструктор родительского класса.
Поэтому, если мы создаём собственный конструктор, мы должны вызвать super , в противном случае объект для this не будет создан, и мы получим ошибку.
Чтобы конструктор Rabbit работал, он должен вызвать super() до того, как использовать this , чтобы не было ошибки:
class Animal < constructor(name) < this.speed = 0; this.name = name; >// . > class Rabbit extends Animal < constructor(name, earLength) < super(name); this.earLength = earLength; >// . > // теперь работает let rabbit = new Rabbit("Белый кролик", 10); alert(rabbit.name); // Белый кролик alert(rabbit.earLength); // 10
Переопределение полей класса: тонкое замечание
Продвинутое замечание
В этом подразделе предполагается, что у вас уже есть определённый опыт работы с классами, возможно, в других языках программирования.
Это даёт лучшее представление о языке, а также объясняет поведение, которое может быть источником ошибок (но не очень часто).
Если вы считаете этот материал слишком трудным для понимания, просто продолжайте читать дальше, а затем вернитесь к нему через некоторое время.
Мы можем переопределять не только методы, но и поля класса.
Однако, когда мы получаем доступ к переопределенному полю в родительском конструкторе, это поведение отличается от большинства других языков программирования.
Рассмотрим этот пример:
class Animal < name = 'animal'; constructor() < alert(this.name); // (*) >> class Rabbit extends Animal < name = 'rabbit'; >new Animal(); // animal new Rabbit(); // animal
Здесь, класс Rabbit расширяет Animal и переопределяет поле name своим собственным значением.
В Rabbit нет собственного конструктора, поэтому вызывается конструктор Animal .
Что интересно, в обоих случаях: new Animal() и new Rabbit() , alert в строке (*) показывает animal .
Другими словами, родительский конструктор всегда использует своё собственное значение поля, а не переопределённое.
Что же в этом странного?
Если это ещё не ясно, сравните с методами.
Вот тот же код, но вместо поля this.name , мы вызываем метод this.showName() :
class Animal < showName() < // вместо this.name = 'animal' alert('animal'); >constructor() < this.showName(); // вместо alert(this.name); >> class Rabbit extends Animal < showName() < alert('rabbit'); >> new Animal(); // animal new Rabbit(); // rabbit
Обратите внимание: теперь результат другой.
И это то, чего мы, естественно, ожидаем. Когда родительский конструктор вызывается в производном классе, он использует переопределённый метод.
…Но для полей класса это не так. Как уже было сказано, родительский конструктор всегда использует родительское поле.
Почему же наблюдается разница?
Что ж, причина заключается в порядке инициализации полей. Поле класса инициализируется:
- Перед конструктором для базового класса (который ничего не расширяет),
- Сразу после super() для производного класса.
В нашем случае Rabbit – это производный класс. В нем нет конструктора constructor() . Как было сказано ранее, это то же самое, как если бы был пустой конструктор, содержащий только super(. args) .
Итак, new Rabbit() вызывает super() , таким образом, выполняя родительский конструктор, и (согласно правилу для производных классов) только после этого инициализируются поля его класса. На момент выполнения родительского конструктора ещё нет полей класса Rabbit , поэтому используются поля Animal .
Это тонкое различие между полями и методами характерно для JavaScript.
К счастью, такое поведение проявляется только в том случае, когда переопределенное поле используется в родительском конструкторе. Тогда может быть трудно понять, что происходит, поэтому мы объясняем это здесь.
Если это становится проблемой, её можно решить, используя методы или геттеры/сеттеры вместо полей.
Устройство super, [[HomeObject]]
Продвинутая информация
Если вы читаете учебник первый раз – эту секцию можно пропустить.
Она рассказывает о внутреннем устройстве наследования и вызов super .
Давайте заглянем «под капот» super . Здесь есть некоторые интересные моменты.
Вообще, исходя из наших знаний до этого момента, super вообще не может работать!
Ну правда, давайте спросим себя – как он должен работать, чисто технически? Когда метод объекта выполняется, он получает текущий объект как this . Если мы вызываем super.method() , то движку необходимо получить method из прототипа текущего объекта. И как ему это сделать?
Задача может показаться простой, но это не так. Движок знает текущий this и мог бы попытаться получить родительский метод как this.__proto__.method . Однако, увы, такой «наивный» путь не работает.
Продемонстрируем проблему. Без классов, используя простые объекты для наглядности.
Вы можете пропустить эту часть и перейти ниже к подсекции [[HomeObject]] , если не хотите знать детали. Вреда не будет. Или читайте далее, если хотите разобраться.
В примере ниже rabbit.__proto__ = animal . Попробуем в rabbit.eat() вызвать animal.eat() , используя this.__proto__ :
let animal = < name: "Animal", eat() < alert(`$ест.`); > >; let rabbit = < __proto__: animal, name: "Кролик", eat() < // вот как предположительно может работать super.eat() this.__proto__.eat.call(this); // (*) >>; rabbit.eat(); // Кролик ест.
В строке (*) мы берём eat из прототипа ( animal ) и вызываем его в контексте текущего объекта. Обратите внимание, что .call(this) здесь неспроста: простой вызов this.__proto__.eat() будет выполнять родительский eat в контексте прототипа, а не текущего объекта.
Приведённый выше код работает так, как задумано: выполняется нужный alert .
Теперь давайте добавим ещё один объект в цепочку наследования и увидим, как все сломается:
let animal = < name: "Животное", eat() < alert(`$ест.`); > >; let rabbit = < __proto__: animal, eat() < // . делаем что-то специфичное для кролика и вызываем родительский (animal) метод this.__proto__.eat.call(this); // (*) >>; let longEar = < __proto__: rabbit, eat() < // . делаем что-то, связанное с длинными ушами, и вызываем родительский (rabbit) метод this.__proto__.eat.call(this); // (**) >>; longEar.eat(); // Error: Maximum call stack size exceeded
Теперь код не работает! Ошибка возникает при попытке вызова longEar.eat() .
На первый взгляд все не так очевидно, но если мы проследим вызов longEar.eat() , то сможем понять причину ошибки. В обеих строках (*) и (**) значение this – это текущий объект ( longEar ). Это важно: для всех методов объекта this указывает на текущий объект, а не на прототип или что-то ещё.
Итак, в обеих линиях (*) и (**) значение this.__proto__ одно и то же: rabbit . В обоих случаях метод rabbit.eat вызывается в бесконечном цикле не поднимаясь по цепочке вызовов.
Картина того, что происходит:
- Внутри longEar.eat() строка (**) вызывает rabbit.eat со значением this=longEar .
// внутри longEar.eat() у нас this = longEar this.__proto__.eat.call(this) // (**) // становится longEar.__proto__.eat.call(this) // то же что и rabbit.eat.call(this);
// внутри rabbit.eat() у нас также this = longEar this.__proto__.eat.call(this) // (*) // становится longEar.__proto__.eat.call(this) // или (снова) rabbit.eat.call(this);
Проблема не может быть решена с помощью одного только this .
[[HomeObject]]
Для решения этой проблемы в JavaScript было добавлено специальное внутреннее свойство для функций: [[HomeObject]] .
Когда функция объявлена как метод внутри класса или объекта, её свойство [[HomeObject]] становится равно этому объекту.
Затем super использует его, чтобы получить прототип родителя и его методы.
Давайте посмотрим, как это работает – опять же, используя простые объекты:
let animal = < name: "Животное", eat() < // animal.eat.[[HomeObject]] == animal alert(`$ест.`); > >; let rabbit = < __proto__: animal, name: "Кролик", eat() < // rabbit.eat.[[HomeObject]] == rabbit super.eat(); >>; let longEar = < __proto__: rabbit, name: "Длинноух", eat() < // longEar.eat.[[HomeObject]] == longEar super.eat(); >>; // работает верно longEar.eat(); // Длинноух ест.
Это работает как задумано благодаря [[HomeObject]] . Метод, такой как longEar.eat , знает свой [[HomeObject]] и получает метод родителя из его прототипа. Вообще без использования this .
Методы не «свободны»
До этого мы неоднократно видели, что функции в JavaScript «свободны», не привязаны к объектам. Их можно копировать между объектами и вызывать с любым this .
Но само существование [[HomeObject]] нарушает этот принцип, так как методы запоминают свои объекты. [[HomeObject]] нельзя изменить, эта связь – навсегда.
Единственное место в языке, где используется [[HomeObject]] – это super . Поэтому если метод не использует super , то мы все ещё можем считать его свободным и копировать между объектами. А вот если super в коде есть, то возможны побочные эффекты.
Вот пример неверного результата super после копирования:
let animal = < sayHi() < alert("Я животное"); >>; // rabbit наследует от animal let rabbit = < __proto__: animal, sayHi() < super.sayHi(); >>; let plant = < sayHi() < alert("Я растение"); >>; // tree наследует от plant let tree = < __proto__: plant, sayHi: rabbit.sayHi // (*) >; tree.sayHi(); // Я животное (. )
Вызов tree.sayHi() показывает «Я животное». Определённо неверно.
- В строке (*) , метод tree.sayHi скопирован из rabbit . Возможно, мы хотели избежать дублирования кода?
- Его [[HomeObject]] – это rabbit , ведь он был создан в rabbit . Свойство [[HomeObject]] никогда не меняется.
- В коде tree.sayHi() есть вызов super.sayHi() . Он идёт вверх от rabbit и берёт метод из animal .
Вот диаграмма происходящего:
Методы, а не свойства-функции
Свойство [[HomeObject]] определено для методов как классов, так и обычных объектов. Но для объектов методы должны быть объявлены именно как method() , а не «method: function()» .
Для нас различий нет, но они есть для JavaScript.
В приведённом ниже примере используется синтаксис не метода, свойства-функции. Поэтому у него нет [[HomeObject]] , и наследование не работает:
let animal = < eat: function() < // намеренно пишем так, а не eat() < . // . >>; let rabbit = < __proto__: animal, eat: function() < super.eat(); >>; rabbit.eat(); // Ошибка вызова super (потому что нет [[HomeObject]])
Итого
- Чтобы унаследовать от класса: class Child extends Parent :
- При этом Child.prototype.__proto__ будет равен Parent.prototype , так что методы будут унаследованы.
- При переопределении конструктора:
- Обязателен вызов конструктора родителя super() в конструкторе Child до обращения к this .
- При переопределении другого метода:
- Мы можем вызвать super.method() в методе Child для обращения к методу родителя Parent .
- Внутренние детали:
- Методы запоминают свой объект во внутреннем свойстве [[HomeObject]] . Благодаря этому работает super , он в его прототипе ищет родительские методы.
- Поэтому копировать метод, использующий super , между разными объектами небезопасно.
- У функций-стрелок нет своего this и super , поэтому они «прозрачно» встраиваются во внешний контекст.
Задачи
Ошибка создания экземпляра класса
важность: 5
В коде ниже класс Rabbit наследует Animal .
К сожалению, объект класса Rabbit не создаётся. Что не так? Исправьте ошибку.
class Animal < constructor(name) < this.name = name; >> class Rabbit extends Animal < constructor(name) < this.name = name; this.created = Date.now(); >> let rabbit = new Rabbit("Белый кролик"); // Error: this is not defined alert(rabbit.name);
Ошибка возникает потому, что конструктор дочернего класса должен вызывать super() .
Вот правильный код:
class Animal < constructor(name) < this.name = name; >> class Rabbit extends Animal < constructor(name) < super(name); this.created = Date.now(); >> let rabbit = new Rabbit("Белый кролик"); // ошибки нет alert(rabbit.name); // White Rabbit
Улучшенные часы
важность: 5
У нас есть класс Clock . Сейчас он выводит время каждую секунду
class Clock < constructor(< template >) < this.template = template; >render() < let date = new Date(); let hours = date.getHours(); if (hours < 10) hours = '0' + hours; let mins = date.getMinutes(); if (mins < 10) mins = '0' + mins; let secs = date.getSeconds(); if (secs < 10) secs = '0' + secs; let output = this.template .replace('h', hours) .replace('m', mins) .replace('s', secs); console.log(output); >stop() < clearInterval(this.timer); >start() < this.render(); this.timer = setInterval(() =>this.render(), 1000); > >
Создайте новый класс ExtendedClock , который будет наследоваться от Clock и добавьте параметр precision – количество миллисекунд между «тиками». Установите значение в 1000 (1 секунда) по умолчанию.
- Сохраните ваш код в файл extended-clock.js
- Не изменяйте класс clock.js . Расширьте его.
class ExtendedClock extends Clock < constructor(options) < super(options); let < precision = 1000 >= options; this.precision = precision; > start() < this.render(); this.timer = setInterval(() =>this.render(), this.precision); > >;
Наследование в JavaScript
Теперь, когда объясняется большая часть подробностей OOJS, эта статья показывает, как создавать «дочерние» классы объектов (конструкторы), которые наследуют признаки из своих «родительских» классов. Кроме того, мы дадим некоторые советы о том, когда и где вы можете использовать OOJS , и посмотрим, как классы рассматриваются в современном синтаксисе ECMAScript.
| Необходимые знания: | Базовая компьютерная грамотность, понимание основ HTML и CSS, знакомство с основами JavaScript (см. Первые шаги и Структурные элементы) and основы Объектно-ориентированного JS (см. Введение в объекты). |
|---|---|
| Цель: | Понять, как можно реализовать наследование в JavaScript. |
Прототипное наследование
До сих пор мы видели некоторое наследование в действии — мы видели, как работают прототипы и как элементы наследуются, поднимаясь по цепочке. Но в основном это связано с встроенными функциями браузера. Как создать объект в JavaScript, который наследует от другого объекта?
Давайте рассмотрим, как это сделать на конкретном примере.
Начало работы
Прежде всего сделайте себе локальную копию нашего файла oojs-class-inheritance-start.html (он также работает в режиме реального времени). В файле вы найдёте тот же пример конструктора Person() , который мы использовали на протяжении всего модуля, с небольшим отличием — мы определили внутри конструктора только лишь свойства:
function Person(first, last, age, gender, interests) this.name = first, last, >; this.age = age; this.gender = gender; this.interests = interests; >
Все методы определены в прототипе конструктора. Например:
Person.prototype.greeting = function () alert("Hi! I'm " + this.name.first + "."); >;
Примечание: . В исходном коде вы также увидите определённые методы bio() и farewell() . Позже вы увидите, как они могут быть унаследованы другими конструкторами.
Скажем так, мы хотели создать класс Teacher , подобный тому, который мы описали в нашем первоначальном объектно-ориентированном определении, которое наследует всех членов от Person , но также включает в себя:
- Новое свойство, subject — оно будет содержать предмет, который преподаёт учитель.
- Обновлённый метод greeting() , который звучит немного более формально, чем стандартный метод greeting() — более подходит для учителя, обращающегося к некоторым ученикам в школе.
Определение функции-конструктора Teacher()
Первое, что нам нужно сделать, это создать конструктор Teacher() — добавьте ниже следующий код:
function Teacher(first, last, age, gender, interests, subject) Person.call(this, first, last, age, gender, interests); this.subject = subject; >
Это похоже на конструктор Person во многих отношениях, но здесь есть что-то странное, что мы не видели раньше — функцию call() . Эта функция в основном позволяет вам вызывать функцию, определённую где-то в другом месте, но в текущем контексте. Первый параметр указывает значение this , которое вы хотите использовать при выполнении функции, а остальные параметры — те, которые должны быть переданы функции при её вызове.
Мы хотим, чтобы конструктор Teacher() принимал те же параметры, что и конструктор Person() , от которого он наследуется, поэтому мы указываем их как параметры в вызове call() .
Последняя строка внутри конструктора просто определяет новое свойство subject , которое будут иметь учителя, и которого нет у Person().
В качестве примечания мы могли бы просто сделать это:
function Teacher(first, last, age, gender, interests, subject) this.name = first, last, >; this.age = age; this.gender = gender; this.interests = interests; this.subject = subject; >
Но это просто переопределяет свойства заново, а не наследует их от Person() , так что теряется смысл того, что мы пытаемся сделать. Он также занимает больше строк кода.
Наследование от конструктора без параметров
Обратите внимание, что если конструктор, от которого вы наследуете, не принимает значения своего свойства из параметров, вам не нужно указывать их в качестве дополнительных аргументов в call() . Так, например, если у вас было что-то действительно простое:
function Brick() this.width = 10; this.height = 20; >
Вы можете наследовать свойства width и height , выполнив это (как и другие шаги, описанные ниже, конечно):
function BlueGlassBrick() Brick.call(this); this.opacity = 0.5; this.color = "blue"; >
Обратите внимание, что мы указали только this внутри call() — никаких других параметров не требуется, поскольку мы не наследуем никаких свойств родителя, которые задаются через параметры.
Установка Teacher()’s prototype и конструктор ссылок
Пока все хорошо, но у нас есть проблема. Мы определили новый конструктор и у него есть свойство prototype , которое по умолчанию просто содержит ссылку на саму конструкторскую функцию. Он не содержит методов свойства prototype конструктора Person . Чтобы увидеть это, введите Object.getOwnPropertyNames(Teacher.prototype) в поле ввода текста или в вашу консоль JavaScript. Затем введите его снова, заменив Teacher на Person . Новый конструктор не наследует эти методы. Чтобы увидеть это, сравните выводы в консоль Person.prototype.greeting и Teacher.prototype.greeting . Нам нужно заставить Teacher() наследовать методы, определённые на прототипе Person() . Итак, как мы это делаем?
-
Добавьте следующую строку ниже своего предыдущего добавления:
Teacher.prototype = Object.create(Person.prototype);
Object.defineProperty(Teacher.prototype, 'constructor', < value: Teacher, enumerable: false, // false, чтобы данное свойство не появлялось в цикле for in writable: true >);
Предоставление Teacher() новой функции greeting()
Чтобы завершить наш код, нам нужно определить новую функцию greeting() в конструкторе Teacher() .
Самый простой способ сделать это — определить его на прототипе Teacher() — добавить в нижнюю часть кода следующее:
Teacher.prototype.greeting = function () var prefix; if ( this.gender === "male" || this.gender === "Male" || this.gender === "m" || this.gender === "M" ) prefix = "Mr."; > else if ( this.gender === "female" || this.gender === "Female" || this.gender === "f" || this.gender === "F" ) prefix = "Mrs."; > else prefix = "Mx."; > alert( "Hello. My name is " + prefix + " " + this.name.last + ", and I teach " + this.subject + ".", ); >;
Это выводит на экран приветствие учителя, в котором используется соответствующий префикс имени для своего пола, разработанный с использованием условного оператора.
Попробуйте пример
Теперь, когда вы ввели весь код, попробуйте создать экземпляр объекта из Teacher() , поставив ниже вашего JavaScript-кода (или что-то похожее по вашему выбору):
var teacher1 = new Teacher( "Dave", "Griffiths", 31, "male", ["football", "cookery"], "mathematics", );
Теперь сохраните, обновите, и попробуйте получить доступ к свойствам и методам вашего нового объекта teacher1 , например:
.name.first; teacher1.interests[0]; teacher1.bio(); teacher1.subject; teacher1.greeting(); teacher1.farewell();
Все должно работать нормально. Запросы в строках 1, 2, 3 и 6 унаследованные от общего конструктора Person() (класса). Запрос в строке 4 обращается к subject , доступному только для более специализированного конструктора (класса) Teacher() . Запрос в строке 5 получил бы доступ к методу greeting() , унаследованному от Person() , но Teacher() имеет свой собственный метод greeting() с тем же именем, поэтому запрос обращается к этому методу.
Примечание: . Если вам не удаётся заставить это работать, сравните свой код с нашей готовой версией (см. также рабочее демо).
Методика, которую мы здесь рассмотрели, — это не единственный способ создания наследующих классов в JavaScript, но он работает нормально и это даёт вам представление о том, как реализовать наследование в JavaScript.
Вам также может быть интересно узнать некоторые из новых функций ECMAScript, которые позволяют нам делать наследование более чисто в JavaScript (см. Classes). Мы не рассматривали их здесь, поскольку они пока не поддерживаются очень широко в браузерах. Все остальные конструкторы кода, которые мы обсуждали в этом наборе статей, поддерживаются ещё в IE9 или ранее и есть способы добиться более ранней поддержки, чем это.
Обычный способ — использовать библиотеку JavaScript — большинство популярных опций имеют простой набор функций, доступных для выполнения наследования более легко и быстро. CoffeeScript , например, предоставляет класс, расширяет и т.д.
Дальнейшее упражнение
В нашем руководстве по Объектно-ориентированному JavaScript для начинающих мы также включили класс Student как концепцию, которая наследует все особенности Person , а также имеет другой метод greeting() от Person , который гораздо более неформален, чем приветствие Teacher . Посмотрите, как выглядит приветствие ученика в этом разделе, и попробуйте реализовать собственный конструктор Student() , который наследует все функции Person() и реализует другую функцию greeting() .
Примечание: . Если вам не удаётся заставить это работать, сравните свой код с нашей готовой версией (см. также рабочее демо).
Object member summary
Подводя итог, вы в основном получили три типа свойств / методов, о которых нужно беспокоиться:
- Те, которые определены внутри функции-конструктора, которые присваиваются экземплярам объекта. Их довольно легко заметить — в вашем собственном коде они представляют собой элементы, определённые внутри конструктора, используя строки this.x = x ; в встроенном коде браузера они являются членами, доступными только для экземпляров объектов (обычно создаются путём вызова конструктора с использованием ключевого слова new , например var myInstance = new myConstructor () .
- Те, которые определяются непосредственно самим конструктором, которые доступны только для конструктора. Они обычно доступны только для встроенных объектов браузера и распознаются путём непосредственной привязки к конструктору, а не к экземпляру. Например, Object.keys() .
- Те, которые определены в прототипе конструктора, которые наследуются всеми экземплярами и наследуют классы объектов. К ним относятся любой член, определённый в свойстве прототипа конструктора, например. myConstructor.prototype.x() .
Если вы не уверены, что это такое, не беспокойтесь об этом, пока вы ещё учитесь и знание придёт с практикой.
Когда вы используете наследование в JavaScript?
В частности, после этой последней статьи вы можете подумать: «У-у-у, это сложно». Ну, ты прав. Прототипы и наследование представляют собой некоторые из самых сложных аспектов JavaScript, но многие возможности и гибкость JavaScript вытекают из его структуры объектов и наследования и стоит понять, как это работает.
В некотором смысле вы используете наследование все время. Всякий раз, когда вы используете различные функции веб-API или методы/свойства, определённые во встроенном объекте браузера, который вы вызываете в своих строках, массивах и т.д., вы неявно используете наследование.
Что касается использования наследования в вашем собственном коде, вы, вероятно, не будете часто его использовать, особенно для начала и в небольших проектах. Это пустая трата времени на использование объектов и наследование только ради этого, когда они вам не нужны. Но по мере того, как ваши базы кода становятся больше, вы с большей вероятностью найдёте необходимость в этом. Если вы начинаете создавать несколько объектов с подобными функциями, то создание универсального типа объекта, содержащего все общие функции и наследование этих функций в более специализированных типах объектов, может быть удобным и полезным.
Примечание: . Из-за того, как работает JavaScript, с цепочкой прототипов и т.д., совместное использование функций между объектами часто называется делегированием. Специализированные объекты делегируют функциональность универсальному типу объекта.
При использовании наследования вам рекомендуется не иметь слишком много уровней наследования и тщательно отслеживать, где вы определяете свои методы и свойства. Можно начать писать код, который временно изменяет прототипы встроенных объектов браузера, но вы не должны этого делать, если у вас нет действительно веской причины. Слишком много наследования могут привести к бесконечной путанице и бесконечной боли при попытке отладки такого кода.
В конечном счёте, объекты — это ещё одна форма повторного использования кода, например функций или циклов, со своими конкретными ролями и преимуществами. Если вы обнаруживаете, что создаёте кучу связанных переменных и функций и хотите отслеживать их все вместе и аккуратно их упаковывать, объект является хорошей идеей. Объекты также очень полезны, когда вы хотите передать коллекцию данных из одного места в другое. Обе эти вещи могут быть достигнуты без использования конструкторов или наследования. Если вам нужен только один экземпляр объекта, вам лучше всего использовать литерал объекта и вам, разумеется, не нужно наследование.
Резюме
В этой статье мы рассмотрели оставшуюся часть основной теории и синтаксиса OOJS, которые, как мы думаем, вам следует знать сейчас. На этом этапе вы должны понимать основы JavaScript, ООП, прототипы и прототипное наследование, как создавать классы (конструкторы) и экземпляры объектов, добавлять функции в классы и создавать подклассы, которые наследуются от других классов.
В следующей статье мы рассмотрим, как работать с JavaScript Object Notation (JSON), общим форматом обмена данными, написанным с использованием объектов JavaScript.
See also
- ObjectPlayground.com — A really useful interactive learning site for learning about objects.
- Secrets of the JavaScript Ninja, Chapter 6 — A good book on advanced JavaScript concepts and techniques, by John Resig and Bear Bibeault. Chapter 6 covers aspects of prototypes and inheritance really well; you can probably track down a print or online copy fairly easily.
- You Don’t Know JS: this & Object Prototypes — Part of Kyle Simpson’s excellent series of JavaScript manuals, Chapter 5 in particular looks at prototypes in much more detail than we do here. We’ve presented a simplified view in this series of articles aimed at beginners, whereas Kyle goes into great depth and provides a more complex but more accurate picture.
- Назад
- Обзор: Objects
- Далее
В этом модуле
- Основы объекта
- Объектно-ориентированный JavaScript для начинающих
- Прототипы объектов
- Наследование в JavaScript
- Работа с данными JSON
- Практика построения объектов
- Добавление функций в нашу демонстрацию прыгающих шаров
Found a content problem with this page?
- Edit the page on GitHub.
- Report the content issue.
- View the source on GitHub.
This page was last modified on 3 авг. 2023 г. by MDN contributors.
Можно ли вызвать метод класса-родителя после его переопределения (Java)?
И снова наступаю на грабли, переходя от теории к практике. Есть абстрактный класс Animal . У этого класса есть метод makeNoise(), который выводит на экран сообщение «Я животное!». Также есть классы Cat и Dog , которые унаследованы от супер-класса Animal . В каждом из них я переопределил метод makeNoise() . Теперь в зависимости от того, кошка была создана, или собака, метод выводит на экран «Мяу» или «Гав». Но! Допустим, появилась необходимость у объекта типа Cat вызвать метод makeNoise() , но не свой метод, который выдаст нам «Мяу», а метод супер-класса, который скажет нам «Я животное». Как это реализовать? Мои попытки ниже, результата 0, вызывается переопределенный метод 🙁
public class Main < public static void main(String[] args) < /*Объекты типа animal*/ Animal cat = new Cat("Маруся"); Animal dog = new Dog("Шарик"); /*Объекты своих собственных типов*/ Cat cat2 = new Cat("Маруся 2"); Dog dog2 = new Dog("Шарик 2"); System.out.println("-------------------"); cat.makeNoise(); cat2.makeNoise(); >> public abstract class Animal < private String name; public void setName(String name) < this.name = name; >public String getName() < name = this.name; return name; >public void makeNoise() < System.out.println("Я животное!!11!!"); >> public class Cat extends Animal < public Cat(String name) < this.setName(name); System.out.println("Новая кошка создана. Ее имя: " + getName()); >@Override public void makeNoise() < System.out.println(getName() + " Сказала: " + "Мяу мяу"); >> public class Dog extends Animal < public Dog(String name) < this.setName(name); System.out.println("Новая собака создана. Ее имя: " + getName()); >@Override public void makeNoise() < System.out.println(getName() + " Сказала: " + "Гав гав"); >>
P.S. Скажите, пожалуйста, присутствует ли в описанном мною примере полиморфизмом?
Отслеживать
1,722 1 1 золотой знак 22 22 серебряных знака 42 42 бронзовых знака
задан 19 авг 2016 в 17:13
Ivan Blohin Ivan Blohin
633 3 3 золотых знака 10 10 серебряных знаков 22 22 бронзовых знака
Переопределение методов — JS: Погружаясь в классы
Устранение дублирования кода — не единственная задача наследования классов. Иногда оно применяется для изменения существующего поведения базового класса.
Тег в браузере представлен классом HTMLSelectElement. У него есть дополнительные методы, которые нужны для работы со списком элементов. Один из таких методов: item(index) . С его помощью можно извлекать конкретный вариант из списка.
name="variants"> Opt 1 Opt 2 Opt 3
// Гипотетический код, который возвращает элемент выше в виде объекта класса HTMLSelectElement const element = document.querySelector('select'); element.item(0); // HTMLOptionElement(textContent="Opt 1") element.item(1); // HTMLOptionElement(textContent="Opt 2")
Представим себе, что нам нужно часто обращаться к элементам этого списка с конца. Для этого постоянно придется выполнять подобный код:
// Свойство length описывает число option элементов внутри select element.item(element.length - 1);
В этом коде нет ничего криминального, но можно лучше. Один из возможных вариантов решения этой задачи состоит в том, чтобы расширить поведение метода и научить его работать с отрицательными числами. Возможность обращаться к индексам в обратном порядке — распространенная практика во многих языках.
// Последний элемент element.item(-1); // HTMLOptionElement(textContent="Opt 3") // Третий с конца element.item(-3); // HTMLOptionElement(textContent="Opt 1")
Как это сделать? Наследование дает возможность переопределять методы суперклассов. Посмотрите на пример:
class HTMLCustomSelectElement extends HTMLSelectElement item(possibleIndex) const realIndex = possibleIndex >= 0 ? possibleIndex : this.length + possibleIndex; // super указывает на родительский класс return super.item(realIndex); > >
Выше создан подкласс HTMLCustomSelectElement, который переопределяет метод item(index) . Переопределение означает, что в подклассе создается метод с тем же именем, что и в родительском классе. Наш новый метод выполняет дополнительную работу по вычислению индекса, но ему все еще нужен исходный метод item(index) , для выборки нужного элемента. Для этого применяется специальный синтаксис, который указывает явно что нужно взять метод из родительского класса: super.item(realIndex) .
Почему понадобился специальный синтаксис? Представьте, что вместо него там был бы такой код:
item(possibleIndex) this.item(possibleIndex); >
Какой в этом случае метод item() нужно брать — в определении которого мы находимся прямо сейчас или родительский? Наследование так устроено, что всегда выбирается тот метод, который находится ближе в цепочке наследования. Поэтому вызов через this породит рекурсию, но родительский метод никогда не будет вызван.
По этой же причине, снаружи объекта невозможно вызвать методы родительского класса, которые были переопределены в наследниках:
const select = new HTMLCustomSelectElement(); // Этот вызов всегда относится к методу item, переопределенному внутри HTMLCustomSelectElement // Вызвать item напрямую из HTMLSelectElement невозможно select.item(3);
Переопределение не ограничивается одним уровнем наследования. Любой переопределенный метод можно снова переопределить в наследниках текущего класса.
Немного по-другому работает вызов конструктора родительского класса. Для этого достаточно использовать super как функцию:
class MyClass extends BaseClass constructor(param) super(param); > >
Использование наследников
Создать класс-наследник и начать его использовать — это две большие разницы. В ситуациях, где эти классы создаются вами, все просто — достаточно заменить вызовы старого класса на новый, но если объекты этого класса создаются чужим кодом, то задача усложняется. Для подмены такого класса, от чужого кода требуется поддержка полиморфного поведения.
Например, при работе с элементами HTML, объекты этих классов иногда порождаются самим программистом, а иногда системой. Например:
// Создаем сами const element1 = new HTMLSelectElement(); // Где-то внутри создается объект HTMLSelectElement const element2 = document.querySelector('select');
Можно ли подменить класс в примере с querySelector() ? Зависит от реализации библиотеки по работе с HTML-элементами. В тех библиотеках, что нам известны, это сделать невозможно. Это значит, что единственный выход использовать собственный класс — это конвертировать вернувшийся объект в объект нужного нам класса. Стоит ли оно того? Почти наверняка, что нет.
const element = document.querySelector('select'); const convertedElement = new MyHTMLSelectElement(element);
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях: