Почти все уважающие себя веб – разработчики используют ООП, и JavaScript программисты не являются исключением. Для того, чтоб начать использовать ООП на JavaScript, достаточно прочитать какой-нибудь мануал на эту тему, либо воспользоваться одной из реализаций классов на JavaScript, но на наш взгляд основная проблема ООП на js не в том, что в нём как – то иначе работает полиморфизм, инкапсуляция или наследование, а в том, что описывать классы приходится через жопу несколько непривычным способом. С целью облегчения жизни простого программиста мы написали небольшую библиотеку _template.js (версия 0.1), которую и представляем на строгий суд общественности.
Скачать исходные коды библиотеки можно здесь
Мы специально отказались от термина «класс» заменив его на «шаблон», чтоб у читателя не было соблазна интерполировать свой опыт общения с классами в других языках на JavaScript.
Первое знакомство
Для начала создадим JavaScript объект c одним static методом, одним public методом и одной private переменной:
var jsObject = function(){ // constructor var name = 'обычный JavaScript объект'; // private this._sayMyName = function(){ // public jsObject._say( 'Привет! я ' + name ); }; this._sayMyName(); }; jsObject._say = function( message ){ // static alert( message ); } document.getElementById( 'create_js_obj' ).onclick = function(){ var obj = new jsObject(); }
При выполнении этого кода будет создан объект obj, который выдаст «Привет! Я обычный JavaScript объект». Метод _sayMyName можно вызвать после создания экземпляра jsObject через obj._sayMyName либо внутри экземпляра объекта через this._sayMyName (как мы и сделали), свойство name доступно только внутри конструктора, а jsObject._say можно вызывать и без создания объекта.
Теперь создадим такой же объект с помощью библиотеки _template.js:
var templateObject = $$._template({ name : $$._private( '_template.js объект' ), _init : $$._default( $$._constructor( function(){ this._sayMyName(); })), _sayMyName : $$._public( function(){ this._say('Привет! я ' + this.name ); }), _say : $$._static( function( message ){ alert( message ); }) }); document.getElementById( 'create_template_obj' ).onclick = function(){ var obj = new templateObject(); };
Возможно сейчас кто – то воскликнет «Эй, а где же тут облегчение жизни простого программиста?!!», но мы ответим «Оно начинается ближе к тысячной строчке кода
». Нет особого смысла использовать ООП если выводишь HelloWorld! при наведении мышки на ссылку, но интуитивно понятное ООП превращается в острую необходимость, когда начинаешь искать нужную функцию в исходниках по Ctrl+F.
Конструкторы
В _template.js предусмотрено 2 вида конструкторов:
- $$._constructor – обычный конструктор объекта;
- $$._superConstructor – конструктор объекта, который перед выполнением пытается сконструировать всех шаблонов – детей данного шаблона (о близкородственных отношениях шаблонов мы расскажем ниже). При этом вызываются только те конструкторы детей, имя которых совпадает с именем суперконструктора, либо которые являются конструкторами по умолчанию.
Для конструкторов доступен модификатор $$._default, с помощью которого можно задать конструктор по умолчанию. Если в шаблоне задан дефолт конструктор, то при создании объекта не обязательно указывать с помощью какого конструктора создавать объект. Т.е. вместо new obj._myConstructor() можно записать new obj() и объект будет создан с помощью конструктора по умолчанию, если таковой имеется в шаблоне.
По секрету говоря, при создании экземпляра объекта из объекта – шаблона, директива new не обязательна:
// экземпляр шаблона var a = new obj._myConstructor(); // абсолютно то же самое var b = obj._myConstructor();
Но с new, конечно, смотрится как – то более «тру».
Конструкторы в действии:
var constructors = $$._template({ log : $$._public( document.getElementById('constructor_log') ), // вызов: constructors._init1(); _init1 : $$._constructor( function(){ this._alert('constructors._init1'); this.sub._initSub1(); }), // вызов: constructors(); _init2 : $$._default( $$._constructor( function(){ this._alert('constructors._init2'); this.sub(); })), // вызов: constructors._initSuper(); _initSuper : $$._superConstructor( function(){ this._alert('constructors._initSuper'); }), _alert : $$._public( function( c ){ this.log.innerHTML += 'конструктор ' + c + ' сработал<br />'; }), sub : $$._public( $$._template({ // вызов: this.sub._initSub1(); _initSub1 : $$._constructor(function(){ this.parent._alert('constructors.sub._initSub1') }), // вызов: this.sub(); _initSub2 : $$._default( $$._constructor(function(){ this.parent._alert('constructors.sub._initSub2'); })), // вызовется сам собой в constructors._initSuper _initSuper : $$._constructor(function(){ this.parent._alert('constructors.sub._initSuper') }) })) }) var a = constructors._init1(), b = constructors(), c = constructors._initSuper();
#constructor_log:
Модификаторы доступа
В _template.js существуют следующие модификаторы доступа:
- $$._public – метод\свойство доступно после создания объекта из шаблона как внутри объекта так и вне его;
- $$._private – метод\свойство доступно после создания объекта только внутри объекта и недоступно шаблонам – детям объекта и шаблону – родителю;
- $$._protected – метод\свойство доступно после создания объекта внутри объекта и внутри шаблонов – детей объекта, но никогда не доступно шаблону – родителю или извне. $$._protected метод объекта – шаблона, если он вызван из шаблонов – детей, видит все свойства объекта – шаблона, кроме $$._private свойств;
- $$._static – метод\свойство доступно извне, даже если объект не создан. Внутри статического метода доступны только другие статические свойства и методы объекта.
На данный момент предполагается, что все конструкторы могут быть только статическими, поэтому конструктору модификатор доступа прописывать не надо.
Усвоим на примере:
var modificators = $$._template({ publicVariable : $$._public( 'Я public переменная' ), privateVariable : $$._private( 'Я private переменная' ), staticVariable : $$._static( 'Я static переменная' ), protectedVariable : $$._protected( 'Я protected переменная' ), _initTest : $$._default($$._constructor(function(){ this._testVars(); this._testVarsStatic(); this.childTpl._initChild(); })), _testVars : $$._public(function(){ tplLog( '<b>Мы находимся в testObj._testVars</b>', this, 'this' ); }), _testVarsStatic : $$._static(function(){ tplLog( '<b>Мы находимся в testObj._testVarsStatic</b>', this, 'this' ); }), childTpl : $$._private( $$._template({ _initChild : $$._constructor(function(){ this._testVars(); }), _testVars : $$._public(function(){ tplLog( '<b>Мы находимся в testObj.childTpl._testVars</b>', this.parent, 'this.parent' ); }) })) }); // функция проверки доступности переменных var tplLog = function(title, obj, objStr ){ var logVars = [ 'publicVariable', 'privateVariable', 'staticVariable', 'protectedVariable' ], log = document.getElementById( 'modificators_log' ); log.innerHTML += '<br /><b>' + title + '</b><br />'; $$._each(logVars, function(n ,v ){ var isFunc = ( typeof obj[v] === 'function' ) value = isFunc? obj[v]() : obj[v], type = ( (value != undefined) && isFunc)? '()' : ''; log.innerHTML += objStr + '.' + v + type +' == ' + value + '<br />'; }) } tplLog( '<b>До создания экземпляра modificators</b>', modificators, 'modificators' ); var testObj = new modificators(); tplLog( '<b>После создания объекта testObj</b>', testObj, 'testObj' );
#modificators_log:
Работа со свойствами и методами экземпляра шаблона
Свойства и методы, определённые в текущем шаблоне доступны через this.имяСвойстваИлиМетода. Если же мы обращаемся к свойству из шаблона – родителя, шаблона – ребёнка или извне, нужно воспользоваться предусмотренным механизмом геттеров и сеттеров.
Примеры:
// изменить значение свойства текущего объекта this.prop = 1; // получить значение свойства текущего объекта var prop = this.prop; // получить значение родительского свойства var prop = this.parent.parentProp(); // изменить значение родительского свойства this.parent.parentProp('Изменённое значение родителя'); // получить значение свойства шаблона - ребёнка var prop = this.subTpl.subProp(); // изменить значение свойства шаблона - ребёнка this.subTpl.subProp('Изменённое значение ребёнка'); // получить значение экземпляра шаблона извне objFromTpl.publicProp(); // изменить значение экземпляра шаблона извне objFromTpl.publicProp('Изменили значение извне');
Для изменения метода экземпляра шаблона предусмотрен метод set(), который _template.js заботливо создаёт для каждого метода. Единственный аргумент метода set() – это функция, которая будет отрабатываться при последующих вызовах метода.
Примеры:
// переопределить родительский метод this.parent.parentMethod.set( function(){ // здесь this - это parent объект, которому мы меняем метод var self = this; /* хотя private свойства и методы не доступны шаблонам - детям, при переопределении родительского метода мы можем ими оперировать */ self.privateVariable = 'приватное свойство'; self.privateMethod(); }); // переопределить public метод экземпляра шаблона извне objFromTpl.publicMethod.set( function(){ return 'Я переопределён!'; });
Переопределить можно только те методы экземпляра шаблона, которые доступны в данном контексте. Даже не пытайтесь переопределить извне private или protected метод или из шаблона – ребёнка переопределить родительский private метод.
Модификация шаблона
В существующий шаблон можно внести изменения с помощью метода extend. Можно изменить значение или модификатор доступа у существующего свойства, добавить или удалить свойство или метод, или подкинуть шаблону ребёнка. При этом новые методы будут только у тех объектов, которые были созданы из объекта – шаблона уже после его расширения. Усвоим на примере. Допустим, существует вот такой шаблон:
var extendTemplate = $$._template({ name : $$._public('Дима'), _init : $$._default( $$._superConstructor( function(){ this._sayMyName(); })), _sayMyName : $$._public( function(){ alert( 'Меня зовут ' + this.name ); }) });
Перед созданием экземпляра объекта – шаблона, скажем ему, что его будут звать не Димой, а Петей:
document.getElementById('create_Petia').onclick = function(){ extendTemplate = extendTemplate._extend({ name : $$._public('Петя') }); new extendTemplate(); };
А теперь наречём наш шаблон почтинепереименовываемым Васей. Для этого подкинем ему шаблона – ребёнка, который будет создаваться перед отработкой конструктора _init и насильно переименовывать родителя в Васю:
document.getElementById('create_Vasia').onclick = function(){ extendTemplate = extendTemplate._extend({ createName : $$._public( $$._template({ _init : $$._constructor( function(){ this.parent.name('Вася'); }) })) }); new extendTemplate(); };
После создания одного Васи, просто так создать Петю уже не получится, т.к. extendTemplate.createName._init будет вызываться при создании объекта и переименовывать кого угодно в Васю. Мы можем радикально решить эту проблему, присвоив undefined и шаблону createName и свойству name:
document.getElementById('create_undefined').onclick = function(){ extendTemplate = extendTemplate._extend({ name : $$._public( undefined ), createName : $$._public( undefined ) }); new extendTemplate(); };
Теперь мы можем создать Петю снова. Ура!
После присваивания значения undefined свойству name и методу createName, если мы нажмём на «Создать Васю», алерт выдаст «Меня зовут undefined», т.к. мы снова добавили метод createName, но свойство name так и осталось неопределённым, а значит не существующим. Перед созданием Васи теперь нужно создать Петю (тем самым вернув шаблону свойство name). Кстати, для определения undefined значения может применяться любой модификатор доступа, т.к. undefined он и в Африке undefined.
Вложенность и обращение к родителям
Шаблоны объектов _template.js могут иметь какую угодно вложенность. К шаблонам – детям можно применять модификаторы доступа, также, как и к обычным свойствам или методам.
var objL1 = $$._template({ name : $$._public ('шаблон L1'), log : $$._public ( document.getElementById('nesting_log') ), _initL1 : $$._default( $$._constructor ( function(){ this._sayMyName( this.name ); new this.objL2(); })), _sayMyName : $$._protected( function( name ){ this.log.innerHTML += 'Привет! я ' + name + '<br />'; }), objL2 : $$._public( $$._template({ name : $$._public ('шаблон L2'), _initL2 : $$._default( $$._constructor ( function(){ this.parent._sayMyName( this.name + ', мой отец - ' + this.parent.name() ); new this.objL3(); })), objL3 : $$._public( $$._template({ name : $$._public ('шаблон L3'), _initL3 : $$._default( $$._constructor ( function(){ this.parent.parent._sayMyName( this.name + ', мой отец - ' + this.parent.name() ); })) })) })) }); document.getElementById('create_nesting_object').onclick = function(){ new objL1(); }
#nesting_log:
Заключение
Вот мы и познакомились с ещё одним способом описания ООП на JavaScript. Надеемся, что библиотека _template.js, если она вообще кому-то пригодится, будет использоваться с любовью и только в благих целях.
И да пребудет с Вами Сила!
