
Всем привет. Сегодня мы с вами разберем как реализовываются классы в Javascript и что такое прототипное наследование. Вообще как таковых классов в Javascript нет. Их реализуют именно с помощью прототипного наследования.
Для начала давайте напишем обычную функцию
var track = function () {
}
Так как любая функция в Javascript является конструктором, то мы можем вызвать ее с помощью оператора new, чтобы создать экземпляр класса.
new track()
вернет нам новый обьект, который является экземпляром класса.
Конструкторы в Javacript пишут с большой буквы, чтобы легко отличить их от обычных функций.
var Track = function () {
}
Итак, давайте запомним с вами следующие понятия. Все, что мы сейчас будем описывать называется класс, функция на которую мы вызываем оператор new - называется конструктор (т.е. наш Track это конструктор), то, что мы получаем в результате вызова new - является экземпляром класса.
Давайте сейчас создадим два экземпляра класса Track, которые будут на вход принимать параметры в виде обьекта.
var track01 = new Track({
name: 'track01',
url: 'track01.mp3'
});
var track02 = new Track({
name: 'track02',
url: 'track02.mp3'
});
Если мы выведем track01 и track02 в консоль - мы увидим, что у нас создались два пустых обьекта.
console.log(track01);
console.log(track02);
Теперь давайте в нашем конструкторе добавим аргумент params и выведем его в консоль
var Track = function (params) {
console.log(params);
}
Как мы видим в консоли - у нас передаются параметры в конструктор и мы можем с ними дальше работать. Переменная this внутри конструктора является контекстом экземпляра и к полям экземпляра можно обращатся через точку. Поэтому мы можем присвоить this.name и this.url, чтобы они были доступны в наших экземплярах класса.
var Track = function (params) {
this.name = params.name;
this.url = params.url;
}
Теперь, если мы обновим страницу мы увидим, что у нас создались экземпляры класса Track с заполненными полями name и url.
Теперь мы можем обращаться с полям экземпляров, например, track01.name или track01.url.
Теперь давайте опишем метод playTrack, чтобы каждый экземпляр класса мог играть.
var Track = function (params) {
this.name = params.name;
this.url = params.url;
this.playTrack = function () {
console.log('We started playing');
}
}
Если мы попробуем вывести this внутри метода playTrack, то увидим что this является экземпляром нашего класса.
console.log('We started playing', this);
Мы можем, например, написать this.name для того, чтобы вывести имя трека.
console.log('We started playing', this.name);
Теперь давайте вызовем
track01.playTrack();
Как мы видим нам вывелось в консоль
We started playing track01
Если мы вызовем этот метод на второй трек, то получим
We started playing track02
Метод класса можно обьявлять не только в конструкторе, как мы только что сделали, но и через свойство prototype.
var Track = function (params) {
this.name = params.name;
this.url = params.url;
//this.playTrack = function () {
//console.log('We started playing', this.name);
//}
}
Track.prototype.playTrack = function () {
console.log('We started playing', this.name);
}
Если мы попробуем запустить наш код, то мы увидим, что он работает абсолютно точно так же.
В чем же различие? Единственная разница в том, что когда метод обьявлен через prototype, а не внутри конструктора, то мы можем его переопределить.
Если мы напишем в консоли
Track.prototype
то это выведет нам обьект с функцией playTrack внутри.
Если мы закомментируем код с prototype и раскомментируем код playTrack внутри конструктора, то мы увидим что Track.prototype в консоли - это пустой обьект, а значит функцию playTrack мы никак не можем изменить
var Track = function (params) {
this.name = params.name;
this.url = params.url;
this.playTrack = function () {
console.log('We started playing', this.name);
}
};
//Track.prototype.playTrack = function () {
//console.log('We started playing', this.name);
//};
###Наследование:
В ООП нас конечно же интересует наследование. Давайте представим, что нам нужен класс YoutubeTrack, который имеет дополнительное поле image и наследуется от класса Track.
var YoutubeTrack = function (params) {
};
var youtubeTrack01 = new YoutubeTrack({
name: 'youtubeTrack01',
url: 'youtubeTrack01.mp3'
});
var youtubeTrack02 = new YoutubeTrack({
name: 'youtubeTrack02',
url: 'youtubeTrack02.mp3'
});
Мы хотим, чтобы наш конструктор YoutubeTrack выполнял все ту же логику, что и Track только с небольшими изменениями. Для этого воспользуемся методом apply. Для того, чтобы вызвать конструктор Track внутри YoutubeTrack напишем
var YoutubeTrack = function (params) {
Track.apply(this, arguments);
};
Что это делает? Apply вызывает функцию Track (в данном случае это конструктор), передавая контекстом this, т.е. контекст YoutubeTrack и передаем все аргументы, которые будут переданы в наш YoutubeTrack.
Давайте выведем в консоль youtubeTrack01 и youtubeTrack02
console.log(youtubeTrack01);
console.log(youtubeTrack02);
Мы видим, что все работает точно так же и name и url выводятся как поля обьектов в консоль, которые у нас засетились из конструктора Track.
Если же мы закоментируем код с Track.apply, то мы увидим, что у нас создались пустые обьекты.
Теперь мы хотим добавить еще одно поле к YoutubeTrack. Назовем его image.
var YoutubeTrack = function (params) {
Track.apply(this, arguments);
this.image = params.image;
};
Так же при создании экземпляров класса YoutubeTrack добавим image
var youtubeTrack01 = new YoutubeTrack({
name: 'youtubeTrack01',
url: 'youtubeTrack01.mp3',
image: 'youtubeTrack01.jpg'
});
var youtubeTrack02 = new YoutubeTrack({
name: 'youtubeTrack02',
url: 'youtubeTrack02.mp3',
image: 'youtubeTrack01.jpg'
});
Как мы видим теперь в консоли у нас выводится еще и image в YoutubeTrack обьектах.
Теперь мы хотим, чтобы все методы, которые нам доступны в классе Track, были так же доступны в YoutubeTrack.
Как мы можем видеть, в экземплярах трека у нас есть функция playTrack и мы хотим, чтобы она была доступна в YoutubeTrack.
Для этого нам нужно реализовать наследование.
YoutubeTrack.prototype = Object.create(Track.prototype);
Мы присваеваем в YoutubeTrack.prototype обьект, которые мы создаем с Track.prototype.
Как мы видим в консоли, у нас появился метод playTrack у экземпляров YoutubeTrack
Теперь мы легко можем вызвать
youtubeTrack01.playTrack();
и будет написано
We started playing youtubeTrack01
Но есть один минус. Если мы напишем в консоли
youtubeTrack.prototype.constructor
мы увидим здесь не конструктор youtubeTrack, а конструктор Track. Как это исправить?
YoutubeTrack.prototype = Object.create(Track.prototype);
YoutubeTrack.prototype.constructor = YoutubeTrack;
Этим мы возвращаем ссылку обратно на конструктор YoutubeTrack как нам необходимо. Теперь если мы посмотрим в консоль, то у нас вызывается правильный конструктор.
Как мы видим теперь у всех экземпляров класса есть метод playTrack и если нам необходимо - мы можем его переопределить.
YoutubeTrack.prototype.playTrack = function () {
console.log('Hello youtube', this.name);
};
Теперь мы можем вызвать свой метод, который нам необходим.