В предыдущей статье я представил на ваше рассмотрение небольшой кусок кода, который позволяет использовать три столпа ООП в JavaScript. Все это достигается несколько хитро, тем не менее я позволил себе чуточку изменить функцию extend, дабы классы имели понятие о том, что такое статические константы (на самом деле константы конечно получились условные, но это, думаю, можно оправдать условностью их в самом JavaScript). Здесь я рассмотрю этот вопрос поподробнее и, видимо, буду расширять статью по мере его более глубокого понимания.
Итак, исходные данные (повторюсь, заимствованы из источников на AJAXPath и на AJAXPatterns):
function Class() { }; Class.prototype.construct = function() { }; Class.extend = function(def) { var classDef = function() { if (arguments[0] !== Class) { this.construct.apply(this, arguments); } }; var proto = new this(Class); var superClass = this.prototype; for (var n in def) { var item = def[n]; if (item instanceof Function) item.$ = superClass; else classDef[n] = item; proto[n] = item; } classDef.prototype = proto; classDef.extend = this.extend; return classDef; };
Благодаря использованию трех этих функций, у вас появляется замечательная возможность строить довольно серьезные и обширные по конструкции фреймворки, не теряя при этом читабельности кода и возможности быстро найти нужное место дабы его изменить. Ну и плюс, конечно, практически все преимущества ООП.
Эти три функции использовались как фундамент ООП-Drag’n'Drop фреймворка для крупного проекта на Java+Wicket. Я бы с удовольствием безвозмездно поделился бы его кодом, но по контракту этот код - собственность компании, а компания не хочет его рассекречивать. По этой причине я могу лишь дать, если нужно, наводящие мысли, наводящие на конкретные мысли :).
Впрочем, ближе к делу. Для такого кода требуется пример. Я наваял тут небольшой скрипт, эмулирующий операционную систему Windows, надеюсь он подойдет:
/* пара вспомогательных функций */ function getInstanceOf(className) { // возвращает объект класса по имени класса return eval('new ' + className + '()'); } function pause(millis) // останавливает выполнение // скрипта на указанное количество миллисекунд { var time = new Date(); var curTime = null; do { curTime = new Date(); } while( curTime - time < millis); } /* === Абстрактная Операционная Система === */ var AbstractOS = Class.extend({ construct: // конструктор, параметр - тип компьютера function(computerClassName) { // компьютер, на котором запускается ОС this._computer = getInstanceOf(computerClassName); }, getComputer: function() { return this._computer; }, reboot: // перезагрузка ОС function() { return this.getComputer().shutDown() && this.getComputer().startUp(); }, shutDown: // выключение ОС function() { return this.getComputer().shutDown(); }, startUp: // запуск ОС function() { return this.getComputer().startUp(); }, exec: // абстрактный (условно) метод запуска команды function(commandStr) { return false; }, cycle: // запуск ОС, выполнение команды, отключение ОС function(cmdStr) { return this.startUp() && this.exec(cmdStr) && this.shutDown(); } }); /* === Синий Экран Смерти === */ var BSOD = Class.extend({ launch: // запуск function() { alert('You see the BSOD'); return true; } }); /* === Операционная Система MS Windows === */ var MSWindows = AbstractOS.extend({ // наследуется от абстрактной ОС // сообщения - статические константы (условно) STARTUP_MSG: 'Windows Starting', EXEC_MSG: 'This program has performed an illegal operation', REBOOT_MSG: 'Do you really want to reboot your computer?', construct: // конструктор, параметр - тип компьютера function(computerClassName) { // вызов родительского конструктора arguments.callee.$.construct.call(this, computerClassName); // кэш-е синего экрана смерти (ибо он будет один) this._bsod = new BSOD(); }, getBSOD: function() { return this._bsod; }, reboot: // перегруженная перезагрузка function() { // вывод сообщения alert(MSWindows.REBOOT_MSG); // вызов родительского метода return arguments.callee.$.reboot.call(this); }, shutDown: // перегруженное выключение function() { // запуск СЭС и, если он удачен - вызов // родительского метода. возвращается результат // удачности return (this.getBSOD().launch() && arguments.callee.$.shutDown.call(this)); }, startUp: // перегруженная загрузка function() { // если удачно выполнился родительский метод if (arguments.callee.$.startUp.call(this)) { // выполнить необходимые операции pause(400); //setTimeout("alert('Windows Starting')", 400); // сообщить об удачной загрузке alert(MSWindows.STARTUP_MSG); return true; } else return false; // нет - так нет }, exec: // перегруженное выполнение команды function(commandStr) { // если команда валидна - выдать результат // исполнения, иначе - выключиться return commandStr ? alert(MSWindows.EXEC_MSG) : this.shutDown(); } }); /* === Обычный Компьтер === */ var SimpleComputer = Class.extend({ startUp: // при запуске выводит сообщение function() { alert('Starting Up'); return true; }, shutDown: // при выключении выводит сообщение function() { alert('Shutting Down'); return true; } }); /* проверочная функция */ function perform() { // инициируем ОС на обычном компьютере (инсталляция) var testOs = new MSWindows('SimpleComputer'); // запускаем ОС testOs.startUp(); // выполняем банальную команду testOs.exec('ls -laF'); // выключаем ОС testOs.shutDown(); }
NB! (не забывайте - после последнего объявления метода в классе запятой ставить не нужно, иначе Ослик (IE) обидится)
Если предыдущий пример вам не понравился — я могу предложить вам довольно полезный класс, который сильно помогает, если в вашем проекте понятие элемента DOM пересекается с понятием объекта, над которым производятся манипуляции:
var ElementWrapper = Class.extend({ construct: function(elementId) { this.elementId = elementId; this.element = null; this._initializeElement(); }, _initializeElement: function() { var docElm = document.getElementById(this.elementId); if (!docElm) { this.element = document.createElement('div'); this.element.id = this.elementId; } else { this.element = docElm; } this._assignListeners(); }, _assignListeners: function() { . . . }, . . . reassignTo: function(elementId) { this.elementId = elementId; this.element = null; this._initializeElement(); } });
От этого класса очень удобно наследовать классы, расширяющие функциональность элементов DOM. Также, теперь вы можете использовать код типа этого:
var someElement = new ElementWrapper('someElmId');
…и объект someElement будет связан с элементом (оборачивать элемент) с id ‘SomeElmId’. Доступ к нему — как к элементу DOM — можно будет получить через свойство someElement.element.
Приведенный ниже класс наследуется от ElementWrapper и позволяет обращаться с обернутым элементом как с практически полноценным (неполноценным? :) ) графическим объектом (используются некоторые функции из предыдущей статьи: getElmAttr, setElmAttr, findOffsetHeight, getPosition, getAlignedPosition)1:
var DEF_NS = 'def'; var DEF_LWIDTH_ATTR = 'localWidth'; var DEF_LHEIGHT_ATTR = 'localHeight'; var DEF_LTOP_ATTR = 'localTop'; var DEF_LLEFT_ATTR = 'localLeft'; var GraphicalElementWrapper = ElementWrapper.extend({ _assignListeners: function() { // do not assign listeners if not required }, setLocalWidth: function(localWidth) { setElmAttr(this.element, DEF_LWIDTH_ATTR, localWidth + 'px', DEF_NS); }, setLocalHeight: function(localHeight) { setElmAttr(this.element, DEF_LHEIGHT_ATTR, localHeight + 'px', DEF_NS); }, setLocalLeft: function(localLeft) { setElmAttr(this.element, DEF_LLEFT_ATTR, localLeft + 'px', DEF_NS); }, setLocalTop: function(localTop) { setElmAttr(this.element, DEF_LTOP_ATTR, localTop + 'px', DEF_NS); }, getLocalWidth: function() { return getElmAttr(this.element, DEF_LWIDTH_ATTR, DEF_NS); }, getLocalHeight: function() { return getElmAttr(this.element, DEF_LHEIGHT_ATTR, DEF_NS); }, getLocalLeft: function() { return getElmAttr(this.element, DEF_LLEFT_ATTR, DEF_NS); }, getLocalTop: function() { return getElmAttr(this.element, DEF_LTOP_ATTR, DEF_NS); }, getOffsetWidth: function() { return this.element.offsetWidth; }, getOffsetHeight: function() { return this.element.offsetHeight || this.element.style.pixelHeight || findOffsetHeight(this.element); }, show: // отобразить function() { this.element.style.display = ''; this.element.style.visibility = 'visible'; }, hide: // спрятать function() { if (this.element.style.display != 'none') { this.element.style.display = 'none'; } }, blank: // "забелить" function() { if (this.element.style.display != '') { this.element.style.display = ''; this.element.style.visibility = 'hidden'; } }, makeBlock: // сделать блоком function() { if (this.element.style.display != 'block') { this.element.style.display = 'block'; } }, isPointInside: // опередляет принадлежность точки элементу function(curPoint) { return (parseInt(this.getLocalLeft()) < curPoint.x) && (parseInt(this.getLocalTop()) 0 ? parseInt(this.getLocalLeft()) : 0) + parseInt(this.getLocalWidth())) > curPoint.x) && (((parseInt(this.getLocalTop()) > 0 ? parseInt(this.getLocalTop()) : 0) + parseInt(this.getLocalHeight())) > curPoint.y); }, isElementNear: // определяет, перекрывает ли другой эл-т // текущий своей большей частью function(graphicalElement) { if (graphicalElement) { var elmCurPos = getPosition(graphicalElement.element); var elmHalfHeight = parseInt(graphicalElement.getLocalHeight())/2; var elmHalfWidth = parseInt(graphicalElement.getLocalWidth())/2; var localLeft = (parseInt(this.getLocalLeft()) > 0 ? parseInt(this.getLocalLeft()) : 0); var localTop = (parseInt(this.getLocalTop()) > 0 ? parseInt(this.getLocalTop()) : 0); var leftCorrect = (elmCurPos.x > (localLeft - elmHalfWidth)) && (elmCurPos.x (localTop - elmHalfHeight)) && (elmCurPos.y < (localTop + parseInt(this.getLocalHeight()) - elmHalfHeight)); return leftCorrect && topCorrect; } else return false; }, recalc: // пересчитывает координаты function() { var pos = getAlignedPosition(this.element); this.setLocalWidth(parseInt(this.getOffsetWidth())); this.setLocalHeight(parseInt(this.getOffsetHeight())); this.setLocalLeft(pos.x); this.setLocalTop(pos.y); } });
Оба этих класса, надеюсь, помогут вам при решении задач, связанных с опознаванием элементов DOM как графических объектов (например, Drag’n'Drop (здесь я наследовал класс перетаскиваемыx нод, классы областей, их содержащих (несколько с разными свойствами, отнаследованных друг от друга) и помощник для перетаскивания — от GraphicElementWrapper, а главный контейнер — от ElementWrapper) или, например, веб-приложение, эмулирующее работу оконного (здесь, когда я этим занимался, я наследовал перетаскиваемые элементы от GraphicElementWrapper, а меню, статусбар, рабочую область — от ElementWrapper).
Как всё это работает — довольно-таки непростой вопрос, но я постараюсь через некоторое время уделить внимание и ему, возможно в этой же статье… А пока — кажется всё. Удач в JS-конструировании :).
Ссылки
про это…
- … - по-русски, от Дмитрия Котерова
- … - более поздние впечатления - по-русски, от Дмитрия Котерова и его соратников
- …на AjaxPatterns
- …на AJAXPath
- …на XML.com
- …на WebReference.com
- …на The Code Project
- …на JavaScript Kit
- …на DevArticles
- … - как на этом делать галерею
- …кратко, от Kevin Lindsey
- …кратко, от Dave Johnson
- … - ссылки от Zeroglif





