Starcode Twitter

_template.js – шаблонизатор объектов JavaScript

дата: 06.11.09
категория: JavaScript, Интсрументарий
тэги: , , ,
комментариев: (0)
  • Digg
  • Facebook
  • Twitter

Почти все уважающие себя веб – разработчики используют ООП, и 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, если она вообще кому-то пригодится, будет использоваться с любовью и только в благих целях.

И да пребудет с Вами Сила!

P.S. Данная статья не преследовала своей целью принижение достоинств JavaScript, не стоит также рассматривать её как попытку притянуть js за уши и приблизить его к «тру олдскул» ООП языкам, вроде C++. Мы любим, ценим и уважаем мультипарадигменность js, библиотека _template.js всего лишь предоставляет, возможно, более удобный для некоторых программистов интерфейс общения с чудесным миром JavaScript.

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">