К сожалению, в своей практике освоения Javascript мне не приходилось встречать толковой реализации классов и механизмов наследования. Хотя сам язык достаточно гибок и имеет огромный потенциал. Не доводилось видеть и удобной организации приватных методов. Обычно для этого использую следующий трюк. В качестве приватного метода создают функцию прямо в конструкторе, где должны быть описаны и все методы использующие её:
function MyClass() { function privateMethod() { } this.publicMethod = function() { // используем privateMethod(); } }
Данный подход имеет недостатки, поскольку privateMethod в указанном примере грубо назвать «методом класса». Все публичные методы приходиться описывать прямо в конструкторе, а значит, они будут создаваться каждый раз при создании нового объекта.
Также невозможно вне конструктора дополнить класс новыми методами.
Хочу предложить собственный механизм реализации классов в Javascript и поделиться одной своей библиотекой — jsOOP.
Библиотека позволяет создавать неймспейсы и классы, наследовать их, добавлять методы и события с определенным уровнем доступа (public, private, protected) и дарит прочие вкусности объектно-ориентированного программирования.
Итак, подробнее о библиотеке.
Сразу приведу пример создания класса:
// объявляем класс A jsOOP.createClass('A') // задаем классу синоним .alias('classA') // задаем опции по умолчанию, они будут переданы конструктору .options({ a: 1 }) // объявляем конструктор .constructor(function(op) { this.log('Call constructor A::A()') .log(op) // вывести опции, переданные в качестве аргумента }) // определяем публичный метод log() .method('log', function(s){ сonsole.log(s); return this; }) // добавляем приватный метод fprv() .privateMethod('fprv', function(){ return this.log('Call public method A::fprv()') }) // определяем защищенный метод fptc() .protectedMethod('fptc', function(){ return this.log('Call protected method A::fptc()') .fprv() // вызываем приватный метод }) // еще один публичный метод fpub() .publicMethod('fpub', function(){ return this.log('Call public method A::fpub()') .fptc() // вызываем защищенный метод })
Объявление класса
Определение нового класса осуществляется очень просто: jsOOP.createClass(«A»). В результате создается класс A в пространстве jsOOP. Класс теперь доступен как jsOOP.A (полный путь), или просто — A. Плюс ко всему, мы добавили синоним классу — classA.
Функция createClass() возвращает ссылку на класс и дальше мы можем продолжать работать с ним: добавлять синонимы, определить конструктор, методы и прочее.
Конструктор
Конструктор определяется при помощи функции constructor(fn). В качестве параметра передается функция, которая будет выполняться при создании экземпляров класса.
Опции
Опции это специальный объект, который передается конструктору при инициализации экземпляра класса. В описании класса при помощи options() мы указали опции по умолчанию. Они могут быть весьма полезны в качестве именованных параметров для конструктора. В дальнейшем доступ к опциям внутри класса можно получить при помощи приватного метода _options().
this._options() // -> {a:2} возвращает опции объекта this._options('a') // -> 2 this._options('a', 3) // устанавливает значение опций
Объявление public, protected, private методов
При помощи method(), publicMethod(), protectedMethod(), privateMethod() мы можем объявлять методы класса. Первым параметром указывается имя метода, вторым функция. method() может принимать третий параметр — уровень доступа, с возможными значениями: «public», «protected», «private» (по умолчанию используется «public»).
Как и в других объектных языках, приватные методы доступны только внутри методов класса. Защищенные, в том числе и внутри потомков класса. А публичные методы везде – как внутри, так и вне класса.
Создание экземпляра класса
Теперь создадим объект данного класса. Это можно выполнить несколькими способами.
// Создаем объект класса var a = new A(); // -> Call constructor A::A() -> {a:1} // другой способ создать объект класса A var a = jsOOP.A.newObject({a:2}); // -> Call constructor A::A() -> {a:2}
Мы видим, что при создании объекта был вызван конструктор, где в качестве первого аргумента ему были переданы опции. Эти опции по умолчанию мы декларировали для данного класса выше при помощи функции options().
Вызов методов
В нашем примере метод fpub, вызывает метод fptc, а тот в свою очередь приватный метод fprv. Но напрямую вызов приватных и защищенных методов генерирует исключение.
a.fpub(); // -> Call public method A::fpub() // -> Call protected method A::fptc() // -> Call public method A::fprv() a.fprv(); // -> throw "Call to private method jsOOP.A::fprv()" a.fptc(); // -> throw "Call to protected method jsOOP.A::fptc()"
Данный механизм, в отличии от прочих реализаций ООП на Javascript, не создает методы внутри конструктора при инициализации каждого объекта, а описывает их один раз в prototype класса.
Атрибуты
Описанный в статье механизм не позволяет организовать уровни доступа для свойств (property) объектов класса. В данной реализации они названы атрибутами. Считаем, что все атрибуты объекта публичны.
// добавляем к классу A атрибут, с указанием значения по умолчанию A.attribute('attr', 123) //... // объект класса var a = new A(); a.log( a.attr ) // -> 123
Свойства
В рамках описанного механизма для поддержки уровня доступа свойств, было принято их организовать в виде методов. Свойства класса объявляются при помощи property(), publicProperty(), protectedProperty() и privateProperty()
// добавляем к классу A свойство prop A.property('myprop', { get: function() { // возвращаем значение внутренней переменной return this._property('myprop') }, set: function(value) { // устанавливаем значение внутренней переменной this._property('myprop', value) } })
В отличии от объявления методов, для свойств можно указывать две функции: get и set, соответственно получения и установки значения свойства. По сути, внутренний механизм создает метод с названием свойства, вызов которого без параметров вызывает функцию get, с параметрами set.
this.myprop(); // возвращает значение this.myprop(234); // устанавливает значение
Если функция get или set в определении свойства опущена, она возвращает/изменяет значение внутренней переменной с названием свойства: this._property(название_свойства)
События
События, как и методы, могут иметь определенный уровень доступа. Добавляются они к классу при помощи функций event(), publicEvent(), protectedEvent(), privateEvent(), где в качестве первого аргумента передаем имя события.
// дополним класс А событием "myevent" jsOOP.A .event('myevent')
Внутренний механизм добавляет к классу метод myevent(). Теперь для экземпляра класса мы можем подписаться на событие и сгенерировать событие.
var a = new A(); // подписываемся на событие a.myevent(function(){ this.log('Обработчик события myevent №1') }) // подписываемся на событие .myevent(function(){ this.log('Обработчик события myevent №2') }) // генерируем событие (запускаем обработчики событий) .myevent() // -> Обработчик события myevent №1 // -> Обработчик события myevent №2
Запуск myevent() с параметрами позволит передать их обработчикам событий в качестве аргументов. Бывает полезно, если нужно передать дополнительную информацию о возникающем событии.
Запуск myevent() с единственным параметром null удаляет всех обработчиков данного события.
Также возможно подписаться на событие сразу же при определении класса. Для этого в определении события event(), помимо названия события вторым аргументом нужно передать функцию обработчик.
// дополним класс А событием "myevent" // и сразу же установим обработчик для всех объектов jsOOP.A .event('myevent', function(){ // обработчик события })
Наследование классов
Наследование класса осуществляется при помощи функции extend()
// объявляем класс B jsOOP.createClass('B') // наследуем класс A .extend('A') // определяем конструктор .constructor(function(op){ // вызов родительского конструктора this._call('parent.constructor', op); this.log('Call constructor B::B()') })
Наследование, как и в некоторых объектных языках можно устроить с определенным уровнем доступа. При использовании privateExtend() все методы родительского класса, включая конструктор, становятся недоступны классу-потомку. protectedExtend() делает публичные методы родительского класса защищенными.
instanceof
Для проверки принадлежности объекта классу используем оператор instanceof
var b = new B(); b instanceof A // -> true b instanceof B // -> true
Предопределенные методы
Для каждого объекта дополнительно создается несколько методов:
* _property — приватный метод, для работы с какими либо внутренними переменными, доступ к которым возможен только внутри методов класса.
// устанавливаем значение внутренней переменной (возвращает this) this._property('privateValue', value) // возвращает значение внутренней переменной this._property('privateValue') // возвращает значения всех переменных this._property()
* _options — приватный метод; возвращает опции, указанные при инициализации объекта
* _class — публичный метод; возвращает класс текущего объекта
* _call — публичный метод; позволяет запускать методы, которые перекрыты классами-потомками
Множественное наследование
В определении класса ничто не мешает использовать extend() для нескольких родительских классов.
jsOOP.createClass('C') .extend('A') .extend('B')
Вызов родительских методов
В случае если класс потомок перекрывает методы родительского класса необходимо воспользоваться предопределенным методом _call.
В качестве первого аргумента функция принимает имя метода с указанием имени родительского класса через точку. Остальные аргументы передаются в качестве аргументов вызова функции.
this._call("A.fprv", arg1, arg2) // тоже что и this._call("jsOOP.A.fprv", arg1, arg2) // тоже что и this._call("classA.fprv", arg1, arg2)
Внутри класса можно в качестве имени класса использовать «self». Таким образом, мы будем точно уверены, что запустится функция именно текущего класса.
Поясню на примере:
//------ class A ------ jsOOP.createClass('A') .constructor(function(){ // запускаем виртуальную функцию func() this.func(); // запускаем именно A::func() this._call('self.func'); }) .method('func', function(){ console.log('Call A::func()') }) //----- class B ------- jsOOP.createClass('B').extend('A') // перезапишем метод func() .method('func', function(){ console.log('Call B::func()') }) var a = new A(); // -> Call A::func() // -> Call A::func() var b = new B(); // -> Call B::func() // -> Call A::func()
Namespace
Каждый объявленный класс является, в том числе и пространством имен. Объявить класс в этом пространстве можно опять же несколькими способами:
// создаем класс D в пространстве имен A jsOOP.A.createClass('D') // или так jsOOP.createClass('A.D') // в том числе можно определить класс в еще несуществующем пространстве jsOOP.createClass('newNamespace.E') // newNamespace будет определено автоматически
Пример использования
Чтобы показать возможности библиотеки, хочу привести более приближенный к жизни пример.
В примере создается два класса: Component и его потомок Button.
(Дополнительно в коде используется фреймворк jQuery)
Класс Component
// объявляем класс Component jsOOP.createClass('Component') .options({ width: 100, height: 100, text: '', parent: document.body // jquery-selector or component }) .constructor(function(op){ this.createElement() .width(op.width) .height(op.height) .text(op.text) .afterRender() }) .protectedMethod('createElement', function() { var par = this._options('parent'); par = $(par instanceof Component? par._element() : par); this._property()._element = $(' <div></div> ').appendTo(par); return this }) // функция возвращает созданный jquery элемент .protectedMethod('_element', function(){ return this._property('_element'); }) // добавляем событие .protectedEvent('afterRender') // добавляем некоторые свойства .property('text', { get: function() { return this._element().text(); }, set: function(value) { this._element().text(value) } }) .property('width', { get: function() { return this._element().width(); }, set: function(value) { this._element().width(value); } }) .property('height', { get: function() { return this._element().height(); }, set: function(value) { this._element().height(value); } }) ;
Создадим для примера два экземпляра класса
// создаем компонент так var com1 = new Component({text: 'First component'}); // или так var com2 = jsOOP.Component.newObject().text('Second component').width(150).height(100);
Класс Button
// Теперь создаем класс Button в пространстве Component jsOOP.Component.createClass('Button') // и наследуем его от Component .extend('jsOOP.Component') // Переустановим некоторые параметры поумолчанию .options({ height: 30 }) .constructor(function(op){ // Запускаем конструктор родительского класса this._call('parent.constructor', op); }) // Добавляем событие click .event('click') // Добавляем обработчик на событие afterRender() .protectedEvent('afterRender', function(){ // при клике на элемент вызываем событие Button::click var self = this; this._element().click(function(e){ self.click(e); }) })
Теперь можно создавать кнопки и назначать им обработчики событий
// указываем полный путь к классу var button1 = new jsOOP.Component.Button({text: 'button-1'}); // или кратко var button2 = new Button({parent:com2}); // (создадим кнопку внутри com2) button2.text('button-2') // назначим обработчик по нажатию на кнопку .click(function(e){ alert('click on button-2') })
Заключение
Надеюсь, статья получилась не слишком длинной и нудной. Я постарался описать кратко все возможности библиотеки.
Постоянные ссылки
При копировании ссылка на TeaM RSN обязательна!