Какое свойство у конструктора позволяет реализовать наследование
Теперь, когда объясняется большая часть подробностей 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);
Здесь наш друг create() снова приходит на помощь. В этом случае мы используем его для создания нового объекта и делаем его значением Teacher.prototype. Новый объект имеет свой прототип Person.prototype и, следовательно, наследует, если и когда это необходимо, все доступные методы Person.prototype. - Нам нужно сделать еще одну вещь, прежде чем двигаться дальше. После добавления последней строки, Teacher.prototype.constructor стало равным Person(), потому что мы просто устанавливаем Teacher.prototype для ссылки на объект, который наследует его свойства от Person.prototype! Попробуйте сохранить код, загрузите страницу в браузере и введите Teacher.prototype.constructor в консоль для проверки.
- Это может стать проблемой, поэтому нам нужно сделать это правильно. Вы можете сделать это, вернувшись к исходному коду и добавив следующие строки внизу:
Object.defineProperty(Teacher.prototype, ‘constructor’, {
value: Teacher,
enumerable: false, // false, чтобы данное свойство не появлялось в цикле for in
writable: true }); - Теперь, если вы сохраните и обновите, введите Teacher.prototype.constructor, чтобы вернуть Teacher(), плюс мы теперь наследуем Person()!
Предоставление 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, например:
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.
В этом модуле
- Основы объекта
- Объектно-ориентированный JavaScript для начинающих
- Прототипы объектов
- Наследование в JavaScript
- Работа с данными JSON
- Практика построения объектов
- Добавление функций в нашу демонстрацию прыгающих шаров
Источник
Прочитав очередную умную книжку про javascript, стал разбираться в методах реализации в нём наследования. Ну, то есть всем конечно понятно, что в реальном, большом, проекте лучше всего для этого использовать функцию из какой-нибудь библиотеки( благо их много ), но ведь хочется понять, как это вообще работает.
Решил посмотреть, что на эту тему есть на хабре — в общем, лучше бы я этого не делал — кто-то пишет статьи про свои костыли на пару страниц потому что библиотечная функция чем-то не понравилась, кому-то кажется удобным доступ к предку по порядковому номеру. в общем много веселья, много кода, споров тоже достаточно а самой базы просто нет. И мне показалось что неплохо бы это исправить.
1. Итак, а в чем собственно проблема?
Проблема в том, что в javascript нет привычного наследования. Максимум что можно сделать, это создать конструктор класса:
function Class(){/*тут инициализируем поля *}
А потом, через прототип, добавить методы, константы и статические переменные, которые будут одни на все экземпляры.
Class.prototype = {/*Методы*/}
Этого в большинстве случаев хватает, но иногда хочется большего.
2. А если очень хочется…
… то можно это реализовать. Классический способ выглядит так:
function inherit_A(Child, Parent)
{
var F = function () { };
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.super = Parent.prototype;
}
И его даже можно использовать:
$(document).ready(function ()
{
function Man(name) { this.name = name }
Man.prototype.say = function () { console.log(“My name is ” + this.name) }
function Gentleman(name) { Man.call(this, name); }
inherit_A(Gentleman, Man);
});
У меня с ним, правда, есть одна маленькая проблема — обычно, для создания прототипа, я использую объектную нотацию, как было показано немного выше:
Class.prototype =
{
/*Методы*/
}
А, с этим наследованием, таким методом не очень попользуешься — или мои функции полностью затрут унаследованный прототип, или унаследованный прототип полностью затрет мои функции. К счастью, наследовать можно и по-другому.
3. Метод посвящается всем любящим объектные литералы
Вот и он:
function inherit_B(Child, Parent)
{
var F = function () { };
F.prototype = Parent.prototype;
var f = new F();
for (var prop in Child.prototype) f[prop] = Child.prototype[prop];
Child.prototype = f;
Child.prototype.super = Parent.prototype;
}
Он просто берет, и копирует все свойства из прототипа родителя в прототип наследника, так что теперь мой любимый объектный литерал работает как надо:
$(document).ready(function ()
{
function Man(name) { this.name = name }
Man.prototype =
{
constructor: Man,
THOUGHTS: “wanna beer!”,
say: function ()
{
console.log(“My name is ” + this.name + ” and i think:'” + this.THOUGHTS + “‘”)
}
}
function Gentleman(name, prefered_beverage)
{
Man.call(this, name);
this.prefered_beverage = prefered_beverage;
}
Gentleman.prototype = { constructor: Gentleman, THOUGHTS: “it’s teatime!” }
inherit_B(Gentleman, Man)
function Programmer(name, prefered_lang)
{
Gentleman.call(this, name, “Cofee”);
this.prefered_lang = prefered_lang;
}
Programmer.prototype =
{
constructor: Programmer,
THOUGHTS: “runtime error 138? wanna debug XD!”
}
inherit_B(Programmer, Gentleman)
var man = new Man(“Jack”);
var gentleman = new Gentleman(“John”, “Orange pekoe”);
var programmer = new Programmer(“James”, “C++”);
man.say();
gentleman.say();
programmer.say();
});
И консоль со мной согласна:
sample.js:11 My name is Jack and i think:’wanna beer!’
sample.js:11 My name is John and i think:’it’s teatime!’
sample.js:11 My name is James and i think:’runtime error 138? wanna debug!’
Отлично. А теперь представим гипотетическую ситуацию, когда родительских классов много, и нужно, например, вызвать какой-нибудь метод, который был перегружен 2-3 класса назад. В такой ситуации, конечно, лучше еще раз хорошенько присмотреться к архитектуре своего приложения. Теперь допустим, что там все работает, как задумано, тогда чертовски неудобно писать, например, так:
this.super.super.super.someMethod.apply(this)
4. Но это тоже решаемо
function inherit_C(Child, Parent)
{
var F = function () { };
F.prototype = Parent.prototype;
var f = new F();
for (var prop in Child.prototype) f[prop] = Child.prototype[prop];
Child.prototype = f;
Child.prototype[Parent.prototype.__class_name] = Parent.prototype;
}
Эта функция в прототип потомка добавляет объект с ссылкой на прототип родителя. Чтобы она заработала как надо, в прототип каждого класса нужно добавить поле
__class_name
Вот, например, предположим, что в нашей иерархии появился класс BadProgrammer, которому как раз понадобился удобный доступ к прототипу далекого предка. Немного модифицируем предыдущий пример:
$(document).ready(function ()
{
function Man(name) { this.name = name }
Man.prototype =
{
__class_name: “Man”,
constructor: Man,
THOUGHTS: “wanna beer!”,
say: function ()
{
console.log(“My name is ” + this.name + ” and i think:'” + this.THOUGHTS + “‘”)
}
}
function Gentleman(name, prefered_beverage)
{
Man.call(this, name);
this.prefered_beverage = prefered_beverage;
}
Gentleman.prototype =
{
__class_name: “Gentleman”,
constructor: Gentleman,
THOUGHTS: “it’s teatime!”
}
inherit_C(Gentleman, Man)
function Programmer(name, prefered_lang)
{
Gentleman.call(this, name, “Cofee”);
this.prefered_lang = prefered_lang;
}
Programmer.prototype =
{
__class_name: “Programmer”,
constructor: Programmer,
THOUGHTS: “runtime error 138? wanna debug XD!”
}
inherit_C(Programmer, Gentleman)
function BadProgrammer(name)
{
Programmer.call(this, name, “brainfuck”);
}
BadProgrammer.prototype =
{
__class_name: “BadProgrammer”,
constructor: BadProgrammer,
THOUGHTS: “runtime error 138? wanna debug XD!”,
say: function () { this.THOUGHTS = this.Man.THOUGHTS; this.Man.say.apply(this); }
}
inherit_C(BadProgrammer, Programmer)
var man = new Man(“Jack”);
var gentleman = new Gentleman(“John”, “Orange pekoe”);
var programmer = new Programmer(“James”, “C++”);
var badprogrammer = new BadProgrammer(“Jake”);
man.say();
gentleman.say();
programmer.say();
badprogrammer.say();
});
Класс BadProgrammer воспользовался мыслями самого первого звена нашей эволюционной цепочки классов, и, теперь, думает совсем не о том, о чём обычно думают программисты 😉
My name is Jack and i think:’wanna beer!’
My name is John and i think:’it’s teatime!’
My name is James and i think:’runtime error 138? wanna debug!’
My name is Jake and i think:’wanna beer!’
5. И еще кое что
Не представляю в какиx случаях это может пригодится, но, возможно, когда-нибудь, кому-нибудь может понадобится множественное наследование. Его тоже вполне можно реализовать:
function inhertit_multiple(child)
{
for( var i = 1; i < arguments.length; ++i )
{
var parent = arguments[i]
for (var prop in parent.prototype)
{
if (!child.prototype[prop]) child.prototype[prop] = parent.prototype[prop];
}
child.prototype[parent.prototype.__class_name] = parent.prototype;
}
}
Не очень-то и сильно отличается от предыдущей версии.
Для того, чтобы показать как оно работает, я придумал еще один вполне реалистичный пример:
$(document).ready(function ()
{
function Mammy() { this.mammy_message = “You Dont love me!” }
Mammy.prototype =
{
__class_name: “Mammy”,
say_something_wise: function () { console.log(this.mammy_message) }
}
function Daddy() { this.daddy_message = “I just don’t want to be a dad!” }
Daddy.prototype =
{
__class_name: “Daddy”,
say_something_wise: function () { console.log(this.daddy_message) }
}
function Lad()
{
this.lad_message = “And i want a candy!”;
Mammy.apply(this);
Daddy.apply(this);
}
Lad.prototype =
{
__class_name: “Lad”,
say_something_wise: function ()
{
this.Daddy.say_something_wise.call(this);
this.Mammy.say_something_wise.call(this);
console.log(this.lad_message);
}
}
inhertit_multiple(Lad, Mammy, Daddy)
var lad = new Lad();
lad.say_something_wise();
});
Если это запустить, то в консоли появится примерно то что мы и ожидаем
I just don’t want to be a dad!
You Dont love me!
And i want candy!
Вот так все это и работает. Весь код и примеры положил сюда — вдруг пригодится.
UPD. Как справедливо заметил один хороший человек, во всех методах, кроме первого, instanceof отказывается признавать класс экземпляром родительского класса. При множественном наследовании этот недостаток сохранился, но для остальных случаев я его устранил.
Источник