
Всем привет! Сегодня мы с вами поговорим о том, что такое миксины в javascript и как они реализуются, а также чем нам может помочь функция extend.
Собственно миксины нам нужны, когда у нас есть несколько классов, которые различаются по логике того, что они делают. Они не могут быть между собой ничем связаны или унаследованы, но у них есть, например, какие-то одинаковые методы. В таких случаях на помощь нам приходят миксины.
Давайте в нашем файле main.js создадим класс Track, который на вход будет принимать name
var Track = function (name) {
this.name = name;
};
Также у нас будет класс Playlist, который на вход будет тоже принимать name
var Playlist = function (name) {
this.name = name;
};
Как мы видим, по своей логике класс Track и класс Playlist делают, впринципе, очень похожие вещи. Они присваивают name. Но на самом деле Track и Playlist сложно наследовать друг для друга по логике. То есть, если бы у нас был какой-то расширенный Track, тогда мы бы могли его наследовать от класса Track. Здесь же нам больше подойдут миксины.
Давайте, например, для Track и Playlist добавим какие-то методы getName через prototype и вернем this.name
var Track = function (name) {
this.name = name;
};
Ttack.prototype.getName = function () {
return this.name;
};
var Playlist = function (name) {
this.name = name;
};
Playlist.prototype.getName = function () {
return this.name;
};
Добавим еще метод play для Track и в нем выведем console.log(this.name + ' started playing'). Такой же метод продублируем и для Playlist.
var Track = function (name) {
this.name = name;
};
Ttack.prototype.getName = function () {
return this.name;
};
Track.prototype.play = function () {
console.log(this.name + ' started playing');
};
var Playlist = function (name) {
this.name = name;
};
Playlist.prototype.getName = function () {
return this.name;
};
Playlist.prototype.play = function () {
console.log(this.name + ' started playing');
};
То есть Playlist у нас тоже имеет метод play и при вызове данного метода будет играть этот Playlist.
Давайте теперь создадим тут же, например, superTrack и на вход ему передадим строку.
var superTrack = new Track('Super track');
Точно также создадим coolPlaylist и передадим
var coolPlaylist = new Playlist('Cool playlist');
Теперь давайте выведем наш Super track и наш coolPlaylist
console.log(superTrack);
console.log(coolPlaylist);
Если мы посмотрим в консоль в браузере, то мы увидим наш Track, у которого есть методы getName и play. Тоже самое с Playlist.
Давайте выведем также
console.log(superTrack.getName());
console.log(coolPlaylist.getName());
В консоли мы видим, что наши имена вывелись. Тоже самое и с оставшимися методами
superTrack.play();
coolPlaylist.play();
Собственно мы написали два класса с методами play и getName, которые очень схожи между собой. Теперь вопрос - как их зарефакторить так, чтобы тот код, который дублируется можно было вынести? Для этого давайте создадим новый объект nameMixin, у которого будет метод getName. Это функция, которая будет возвращать this.name.
var Track = function (name) {
this.name = name;
};
// Track.prototype.getName = function () {
// return this.name;
// };
// Track.prototype.play = function () {
// console.log(this.name + ' started playing');
// };
var Playlist = function (name) {
this.name = name;
};
// Playlist.prototype.getName = function () {
// return this.name;
// };
// Playlist.prototype.play = function () {
// console.log(this.name + ' started playing');
// };
var nameMixin = {
getName: function() {
return this.name;
}
};
var superTrack = new Track('Super track');
var coolPlaylist = new Playlist('Cool playlist');
console.log(superTrack);
console.log(superTrack.getName());
superTrack.play();
console.log(coolPlaylist);
console.log(coolPlaylist.getName());
coolPlaylist.play();
И создадим еще один mixin, запишем его тут же после nameMixin, назовем его controlsMixin, в который мы могли бы вынести все методы, которые могут управлять как Track, так и Playlist одновременно.
var controlsMixin = {
play: function() {
console.log('this.name' + ' started playing');
}
};
Как теперь применить эти методы на наши классы? Для этого используется функция extend. Она, например, есть в JQuery. Если у вас на странице index.html подключен в head скрипт JQuery (у меня он подключен), то вы можете написать, например, в файле main.js
$.extend(Track.prototype, nameMixin, controlsMixin);
Первым параметром мы передаем объект, который хотим заэкстендить (в данном случае это будет Track.prototype), а вторым,третьим и так далее параметрами - то, чем мы хотим заэкстендить, то есть те объекты, которые мы хотим подмешать к prototype (в нашем случае - это будет nameMixin и controlsMixin).
Реализуем все тоже самое для Playlist.prototype
$.extend(Playlist.prototype, nameMixin, controlsMixin);
При обновлении браузера, в консоли мы не увидим изменений, все методы также вызываются и все отрабатывает, не смотря на то, что мы закомментировали два куска кода. Мы можем их удалить вообще, так как теперь они нам не нужны. Итак, что у нас получается:
var Track = function (name) {
this.name = name;
};
var Playlist = function (name) {
this.name = name;
};
var nameMixin = {
getName: function () {
return this.name;
}
};
var controlsMixin = {
play: function () {
console.log('this.name' + ' started playing');
}
};
$.extend(Track.prototype, nameMixin, controlsMixin);
$.extend(Playlist.prototype, nameMixin, controlsMixin);
var superTrack = new Track('Super track');
var coolPlaylist = new Playlist('Cool playlist');
console.log(superTrack);
console.log(superTrack.getName());
superTrack.play();
console.log(coolPlaylist);
console.log(coolPlaylist.getName());
coolPlaylist.play();
У нас есть класс Track, а также класс Playlist. И у нас есть два миксина, содержащие нужные нам методы, которые мы шарим. Extend в JQuery помогает нам подмешивать эти два миксина для Track.prototype и Playlist.prototype, что дает возможность вызывать эти методы. Поэтому, если мы сейчас посмотрим в консоли браузера, то у Track есть getName и play через prototype.
Теперь давайте разберемся что делает функция extend в принципе и как она работает в JavaScript. Для этого вверху файла main.js создадим функцию extend, которая на вход будет принимать target. Это как раз и будет такой же объект, как и в JQuery - объект, к которому мы хотим что-то подмешать.
var extend = function(target) {
if(!arguments[1]) {
return;
}
for(i=1; i < arguments.length; i++) {
var source = arguments[i];
console.log(source);
for(var prop in source) {
if(!target[prop] && source.hasOwnProperty(prop)) {
target[prop] = source[prop];
}
}
}
};
Напомню, что arguments - в любой функции выведет массив всех аргументов, которые переданы на вход (например у $.extend(Track.prototype, nameMixin, controlsMixin); - arguments выведет массив из трех аргументов).
Далее сделаем проверку - если arguments[1] у нас не существует, то выполняется return. Этим самым мы говорим, что если у нас есть только один аргумент, то мы выходим из функции extend и ничего не делаем, поскольку нам нечего подмешивать.
Далее, напишем цикл for, в котором пройдемся по всем аргументам, начиная с первого. Почему с первого? Потому что проходить по элементу, к которому мы хотим подмешать наши миксины, нам не нужно. Внутри цикла пропишем var source = arguments[i]. Собственно, мы проходимся по аргументы, начиная с первого и теперь, если мы выведем console.log(source) - то мы увидим, что это будут наши аргументы.
Теперь добавим еще один цикл for уже по объекту source. Внутри проверяем, если в нашем таргете, то есть в том элементе, в котором мы ходим подмешать миксины, нет объекта property, то мы будем делать то, что нам нужно.
Что такое hasOwnProperty(prop)? Это метод, который проверяет есть ли именно такой property у объекта. Но это не все. Самое главное зачем мы ее здесь написали - нам нужны только методы, которые пренадлежат этому объекту, а не переданы ему по цепочке прототипов.
Внутри проверки мы присваиваем target[prop] = source[prop]. То есть мы просто берем наш target, берем prop (то есть текущую функцию или property) и делаем просто присваивание, то есть ссылку.
Теперь давайте попробуем. Уберем знаки $ и оставим просто extend, который мы написали сами.
extend(Track.prototype, nameMixin, controlsMixin);
extend(Playlist.prototype, nameMixin, controlsMixin);
Давайте посмотрим в консоль браузера. Как мы видим, наш код отрабатывает также, как и раньше.
Давайте еще раз пройдемся по функции extend, чтобы все точно поняли. На вход мы принимаем target, то есть объект Track.prototype и Playlist.prototype. Далее, все остальные аргументы, которые будут, мы проходим первым циклом for. Мы присваиваем source номер аргумента, чтобы с ним работать и проходимся по каждому аргументу (nameMixin и controlsMixin). Берем все property данного объекта и присваиваем их в наш объект - Track.prototype и Playlist.prototype, только при условии, что в Track и Playlist нет уже таких объектов, а также при условии, что getName и play - не наследованы по цепочке прототипов и принадлежат именно этому объекту.
Вот так вот очень легко можно использовать миксины в JavaScript.