Mootools - не только для эффектов

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

Цикл рассчитан на программиста, уже уверенно пишущего на JS, который хочет использовать этот замечательный язык не только для красивых эффектов, но и для построения серьезных, высоконагруженных веб-приложений, где браузер пользователя выполняет от 70 до 100% бизнес-логики.  Всего запланировано примерно 10 статей по темам: события и цепочки событий, классы и объекты Mootools, наследование и полиморфизм, распределенная модель MVC и AJAX, WebSockets и LocalStorage, особенности локализации JavaScript-приложений.

Сразу оговорюсь, почему Mootools, а не гораздо более популярный jQuery. Все просто: jQuery реализует только общепринятое prototype-наследование. Так тоже можно, но классы есть классы, и этим всё сказано. Как подсказывает практика, проект, который больше, чем слайдер изображений без объектов и наследования тяжело написать, и ещё тяжелее поддерживать. Также, Mootools уже имеет большое количество удобных встроенных классов, например Locale, позволяющий "на лету" локализовать интерфейс пользователя без перезагрузки страницы, или Events, позволяющий создавать собственные обработчики событий. Но, по порядку... 

 

Классы в Mootools, наследование и полиморфизм или "JS по-взрослому".

Пока мы ждем официальной поддержки JS 2.0 или волшебного dart от Google, где все прелести объектно-ориентированного программирования будут доступны "из коробки" проекты все-таки надо писать, сдавать и поддерживать. Разработчики Mootools, помаявшись с prototype, решили реализовать собственный объект Class.

 

По сути Class - это функция, принимающая один параметр типа "object". Class имеет предопределенные свойства: initialize, toElement, Implements, Extends.

 

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

 

Пример:
var vehicle = new Class({ //Транспортное средство 
    //Свойства 
    wheels:13, 
    tires:'', 
    brand:'', 
    model:'', 
    engine:0, 
    transmission:'', 
    //Методы  
    initialize:function(properties){ //Конструктор 
        if(typeOf(properties)=='object'){  
            Object.each(properties,function(property,key){ 
                this[key] = property; 
            }.bind(this)); 
        } 
    }, 
    drive:function(from,to){ //Ехать 
        console.log('Едем из '+from+' в '+to); 
    } 
}); 
 
var car = new Class({ //Легковой автомобиль 
    //Наследуем 
    Extends:vehicle, 
    //Свойства 
    racer:false, 
    //Методы 
    initialize:function(properties){ //Конструктор 
        this.parent(properties); 
        if(this.racer){ 
            alert('WROM-WROOOM'); 
        } 
    },  
    drive:function(from,to){ //Ехать (перегружен) 
        this.parent(from,to); 
        alert ('Car arrived'); 
    }  
}); 
 
var truck = new Class({ // Грузовик 
    //Наследуем 
    Extends:vehicle, 
    //Свойства 
    capacity:0, 
    weight:0, 
    transmission:'mechanic', //Определили значение свойства по умолчанию 
    //Методы 
    //Конструктора нет -> используем конструктор класса vehicle 
    charge:function(addTo){ // Загрузить 
        this.weight += ((this.capacity - this.weight) > addTo)?addTo:this.capacity - this.weight; 
        return this.weight; 
    }, 
    unload:function(match){ // Разгрузить 
        this.weight = Math.max(this.weight - match,0); 
        return this.weight; 
    } 
})
Разбираем код

Создали 3 класса:

 

vehicle - описывает транспортное средство. У любого ТС есть размер диска, шины, производитель, модель, двигатель, трансмиссия. Также любое транспортное средство умеет перемещаться из точки А в Б (просто напишет в консоль сообщение).
Метод initialize здесь принимает стандартный JavaScript объект, затем просто читает все свойства переданного объекта и записывает их в свойства нашего вновь создаваемого объекта.

 

car - легковушка. Свойство Extends определяет, что car потомок vehicle т.е. имеет все свойства vehicle и метод drive. Дополнительно есть свойство racer - гоночный или нет. Если гоночный, то при создании объекта сделает 'WROM-WROOM'.

 

В классе car мы сделали перегрузку методов initialize и drive, т. е. изменили родительские методы.

 

Важно: в Mootools вызов родительского метода из дочернего осуществляется простой конструкцией this.parent(<params>).

 

truck - грузовик. Также потомок vehicle. Имеет собственные свойства capacity (вместимость), weight (текущую загрузку) и собственные методы charge (загрузить) и unload(выгрузить). Заметим, что initialize для класса truck мы не определям.

 

Важно: если конструктор (функция initialize) в потомке не изменяется по отношению к конструктору родителя, то в потомке ее можно не описывать, по умолчанию при создании объекта класса-потомка будет автоматичекси вызван конструктор родителя

 

Теперь создадим парочку объектов:
var Mustang = new car({'wheels':18,tires:'GoodYear','brand':'Ford','model':'Mustang','engine':4000, racer:true});
var BigTruck = new truck({'wheels':19.5,'tires':'Micheline','brand':'КамАЗ','model':'КАМАЗ-4308-А3','capacity':3000,engine':6700});

 

Элементарная проверка показывает, что Mustang и BigTruck - действительно объекты разных классов, классы не передали свои свойства и методы друг другу и родительский класс от наших манипуляций не изменился, как это часто бывает при некорректном prototype - наследовании. Попробуйте, например, вызвать метод charge объекта Mustang - получите ошибку, т.к. такого метода у класса car нет.

 

Создадим еще один объект:
var AnoterTruck = new truck({'wheels':22.5,'tires':'Bridgestone','brand':'КрАЗ','capacity':5000,engine:8000});

 

Попробуем загрузить оба грузовика:
BigTruck.charge(1000);
AnoterTruck.charge(3000);

 

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

 

Implements: зачем нужно и в чем отличие от Extends?

Классический пример - встроенные классы Mootools: Options и Events.

 

Можно сказать, что это "вспомогательный способ" наследования. Implements может принимать массив объектов, можно "унаследовать" свойства и методы не от одного класса, а от нескольких. Но это не наследование в полном смысле, т.к. вызвать метод родителя способом this.parent(<parameters>) не получится. Значит, если Вы зачем-то перегружаете метод имплементированного объекта, то весь функционал родительского метода можно считать потерянным в данном классе и его объектах. То есть, работает этот механизм примерно следующим образом: если в создаваемом классе явно не определено свойство или метод имплементируемого класса, то это свойство или этот метод добавляются к создаваемому классу. Ключевое слово "явно". Здесь есть ловушка.

Допустим, Вы пишете такой код:

 

var Animal = new Class({
 initialize: function(age){
 this.age = age;
 }
});
var Animal2 = new Class({
 initialize:function(another){
 this.weight = another+" кг."
 }
});
var Cat = new Class({
 Implements: [Animal,Animal2], 
 setName: function(name){
 this.name = name
 }
});
var myAnimal = new Cat(15);
myAnimal.setName('Micia');

 

Заметим, что в классе Cat нет метода initialize. Но при создании объекта myAnimal конструктор вызывается. Так какой же конструктор будет использован: Animal или Animal2? Ответ: будет использован конструктор класса, имплементированного ПОСЛЕДНИМ т.е. последнего элемента массива Implements! Кто позже встал, того и тапки. Нелогично, но так есть. То есть в этом примере у объекта myAnimal нет свойства age, зато есть свойство weight. И весит кошечка Micia 15 кг. Жалко животное :(

 

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

 

Метод объекта Class implement()

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

 

Классический пример из документации: 
var Animal = new Class({
 initialize: function(age){
 this.age = age;
 }
});
Animal.implement({
 setName: function(name){
 this.name = name;
 }
});
var myAnimal = new Animal(20);
myAnimal.setName('Micia');

 

То есть, создали класс Animal, затем добавили к нему метод setName. Всё работает, в чем подвох?

 

Изменим пример и добавим изначально в класс Animal метод setName вот так:

 

var Animal = new Class({
 initialize: function(age){
 this.age = age;
 },
 setName:function(name){
 this.name = name;
 alert('Meou');
 }
});
Animal.implement({
 setName: function(name){
 this.name = name;
 }
}); 
var myAnimal = new Animal(20);
myAnimal.setName('Micia');

 

Будет ли кошка мяукать при создании объекта класса? Нет, не будет, потому что implement() полностью переопределил метод setName класса. Кошку лишили права голоса. Это действует и на ранее созданные объекты.
Важно: при использовании метода implement() перегружайте уже существующие методы только если точно уверены, что делаете. Поиск бага может занимать часы и дни.

 

Напоследок: простенько, но крайне удобно: toElement() или превращаем объект в HTML-узел.

toElement() - еще один (последний) из предопределенных методов объекта Class. Он отвечает за связь функции $() и нашего объекта. То есть, если мы хотим, чтобы наш объект представлял какой-то HTML-элемент или узел, мы можем указать, что по умолчанию ЭТО выглядит именно так.

Пример:

Возьмем класс vehicle из Примера 1 и дополним его:

var vehicle = new Class({
wheels:13,
tires:'',
brand:'',
model:'',
engine:0,
transmission:'',
image:false,
initialize:function(properties){
this.img: new Element('img',{'src':'noImage.png'});>
if(typeOf(properties)=='object'){
Object.each(properties,function(property,key){
this[key] = property;
if(this.image){
this.img.set('src',this.image);
}
}.bind(this));}
},
drive:function(from,to){
console.log('Едем из '+from+' в '+to);
},
toElement:function(){
return this.img;
}
});

 

Мы ввели дополнительное свойство image. Сюда будем передавать ссылку на картинку. Далее, в конструкторе ввели еще односвойство - img и сразу создали новый элемент <img> пока с пустым изображением по умолчанию. Если в объект будет передана ссылка на изображение, тогда просто заменим атрибут src тега img. Теперь у нас есть изображение нашего транспортного средства.

Далее добавили метод toElement(). Он очень простой - возвращает ссылку на нашу картинку. Классы car и truck оставляем без изменения. Теперь мы можем сделать так:

 

var Subaru = new car({'wheels':18,tires:'Nokian','brand':'Subaru','model':'Impreza WRX STI','engine':2457,image:'mySuby.png', racer:true});

 

Теперь можно просто вставить картинку в body или любой другой узел DOM:

 

$(document.body).appendChild( $(Subaru));

 

Вот так просто.

 

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

 

Добавить комментарий


Защитный код
Обновить