ООП и JavaScript
JavaScript

В предыдущей статье я представил на ваше рассмотрение небольшой кусок кода, который позволяет использовать три столпа ООП в 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-конструировании :).

Ссылки

про это…

(Sun, 19 Aug 2007 at 0229.55)

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License