Ни слова о луке

сэр шаман рассказывает о чём может

ООП и JavaScript

В предыдущей статье я представил на ваше рассмотрение небольшой кусок кода, который позволяет использовать три столпа ООП в JavaScript. Все это достигается несколько хитро, тем не менее я позволил себе чуточку изменить функцию extend, дабы классы имели понятие о том, что такое статические константы (на самом деле константы конечно получились условные, но это, думаю, можно оправдать условностью их в самом JavaScript). Здесь я рассмотрю этот вопрос поподробнее и, видимо, буду расширять статью по мере его более глубокого понимания.

Итак, исходные данные (повторюсь, заимствованы из источников на AJAXPath и на AJAXPatterns):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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, надеюсь он подойдет:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/* пара вспомогательных функций */

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 пересекается с понятием объекта, над которым производятся манипуляции:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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. Также, теперь вы можете использовать код типа этого:

1
var someElement = new ElementWrapper('someElmId');

…и объект someElement будет связан с элементом (оборачивать элемент) с id SomeElmId. Доступ к нему – как к элементу DOM – можно будет получить через свойство someElement.element.

Приведенный ниже класс наследуется от ElementWrapper и позволяет обращаться с обернутым элементом как с практически полноценным (неполноценным? :) ) графическим объектом (используются некоторые функции из предыдущей статьи: getElmAttr, setElmAttr, findOffsetHeight, getPosition, getAlignedPosition)

Как и у некоторых функций из предыдущей статьи, со временем код изменился – в данном случае он оброс функциональностью и одновременно несколько упростился:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
var DND_NS              = 'dnd'; // to use in getAttributeNS and setAttributeNS

var DND_LWIDTH_ATTR     = 'localWidth';
var DND_LHEIGHT_ATTR    = 'localHeight';
var DND_LTOP_ATTR       = 'localTop';
var DND_LLEFT_ATTR      = 'localLeft';
var DND_BTOP_ATTR       = 'baseTop';
var DND_BLEFT_ATTR      = 'baseLeft';

var GraphicalElementWrapper = ExpandedElementWrapper.extend({

    _assignListeners:
        function() {
            // не назначать слушателей событий, если не необходимо
        },

    // ========[ функции установки необходимых для работы аттрибутов ]==========

    /* localLeft, localTop -- атрибуты, содержащие координату верхнего левого угла элемента
                              с учётом смещения [скроллинга];
       localWidth, localHeight -- атрибуты, содержащие реальную высоту и ширину элемента;
       baseLeft, baseTop -- атрибуты, содержащие координату верхнего левого угла элемента
                            без учёта смещения [скроллинга] */

    setLocalWidth:
        function(localWidth) {
            setElmAttr(this.element, DND_LWIDTH_ATTR, localWidth + 'px', DND_NS);
        },

    setLocalHeight:
        function(localHeight) {
            setElmAttr(this.element, DND_LHEIGHT_ATTR, localHeight + 'px', DND_NS);
        },

    setLocalLeft:
        function(localLeft) {
            setElmAttr(this.element, DND_LLEFT_ATTR, localLeft + 'px', DND_NS);
        },

    setLocalTop:
        function(localTop) {
            setElmAttr(this.element, DND_LTOP_ATTR, localTop + 'px', DND_NS);
        },

    setBaseLeft:
        function(baseLeft) {
            setElmAttr(this.element, DND_BLEFT_ATTR, baseLeft + 'px', DND_NS);
        },

    setBaseTop:
        function(baseTop) {
            setElmAttr(this.element, DND_BTOP_ATTR, baseTop + 'px', DND_NS);
        },

    getLocalWidth:
        function() {
            return getElmAttr(this.element, DND_LWIDTH_ATTR, DND_NS);
        },

    getLocalHeight:
        function() {
            return getElmAttr(this.element, DND_LHEIGHT_ATTR, DND_NS);
        },

    getLocalLeft:
        function() {
            return getElmAttr(this.element, DND_LLEFT_ATTR, DND_NS);
        },

    getLocalTop:
        function() {
            return getElmAttr(this.element, DND_LTOP_ATTR, DND_NS);
        },

    getBaseLeft:
        function() {
            return getElmAttr(this.element, DND_BLEFT_ATTR, DND_NS);
        },

    getBaseTop:
        function() {
            return getElmAttr(this.element, DND_BTOP_ATTR, DND_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: // находится ли точка внутри элемента, точка в формате {x, y}
        function(curPoint) {
            var localRight  = parseInt(this.getLocalLeft()) + parseInt(this.getLocalWidth())
                                       + this.element.scrollLeft;
            var localBottom = parseInt(this.getLocalTop())  + parseInt(this.getLocalHeight())
                                       + this.element.scrollTop;
            return (parseInt(this.getLocalLeft()) < curPoint.x) &&
                   (parseInt(this.getLocalTop())  < curPoint.y) &&
                   (localRight > curPoint.x) && (localBottom > curPoint.y);
        },

    isElementNear: /* находится ли переданный элемент рядом с этим элементом
            (перекрывает область этого элемента больше чем половиной своей) */
        function(graphicalElement) {
            if (graphicalElement) {
                var elmCurPos = findPos(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 < (localLeft + parseInt(this.getLocalWidth()) - elmHalfWidth));
                var topCorrect = (elmCurPos.y > (localTop - elmHalfHeight)) &&
                                 (elmCurPos.y < (localTop + parseInt(this.getLocalHeight()) - elmHalfHeight));
                return leftCorrect && topCorrect;
            } else return false;
        },

    isElementInside: // находится ли переданный элемент внутри этого элемента
        function(graphicalElement) {
            if (graphicalElement) {
                var elmCurPos = findPos(graphicalElement.element);
                var elmHalfHeight = parseInt(graphicalElement.getOffsetHeight())/2;
                var elmHalfWidth = parseInt(graphicalElement.getOffsetWidth())/2;
                return this.isPointInside({x:(elmCurPos.x + elmHalfWidth),
                                       y:(elmCurPos.y + elmHalfHeight)})
            } else return false;
        },

    isLeftSide: // находится ли точка({x, y}) на левой стороне области элемента
        function(curPoint) {
            var elmHalfWidth = parseInt(this.getLocalWidth())/2;
            var localLeft = (parseInt(this.getLocalLeft()) > 0 ? parseInt(this.getLocalLeft()) : 0);
            return (curPoint.x >= localLeft) && (curPoint.x < (localLeft + elmHalfWidth));
        },

    isRightSide: // находится ли точка({x, y}) на правой стороне элемента
        function(curPoint) {
            var elmHalfWidth = parseInt(this.getLocalWidth())/2;
            var localRight = ((parseInt(this.getLocalLeft()) > 0
                    ? parseInt(this.getLocalLeft())
                    : 0)) + parseInt(this.getLocalWidth());
            return (curPoint.x <= localRight) && (curPoint.x > (localRight - elmHalfWidth));
        },

    inTopOf: // находится ли точка({x, y}) на верхней стороне области элемента
        function(curPoint) {
            var localTop    = (parseInt(this.getLocalTop()) > 0 ? parseInt(this.getLocalTop()) : 0);
            var localHeight = (parseInt(this.getLocalHeight()) > 0 ? parseInt(this.getLocalHeight()) : 0);
            if (this.element.clientHeight && (this.element.clientHeight < localHeight))
                localHeight = this.element.clientHeight;
            return ((curPoint.y > localTop) && (curPoint.y <= (localTop + (localHeight / 10))));
        },

    inBottomOf: // находится ли точка({x, y}) на нижней стороне области элемента
        function(curPoint) {
            var localTop    = (parseInt(this.getLocalTop()) > 0 ? parseInt(this.getLocalTop()) : 0);
            var localHeight = (parseInt(this.getLocalHeight()) > 0 ? parseInt(this.getLocalHeight()) : 0);
            if (this.element.clientHeight && (this.element.clientHeight < localHeight))
                localHeight = this.element.clientHeight;
            return ((curPoint.y >= (localTop + localHeight - (localHeight / 10))) &&
                    (curPoint.y < (localTop + localHeight)));
        },

    recalc: // пересчитывает координаты элемента
           /* baseOffset в подавляющем большинстве случаев -- это
           { x: this.element.scrollLeft, y: this.element.scrollTop } */
        function(baseOffset) {

            var pos = findPos(this.element);

            this.setBaseLeft(pos.x);
            this.setBaseTop(pos.y);
            this.setLocalLeft(pos.x - (baseOffset ? baseOffset.x : 0));
            this.setLocalTop(pos.y - (baseOffset ? baseOffset.y : 0));
            this.setLocalWidth(parseInt(this.getOffsetWidth()));
            this.setLocalHeight(parseInt(this.getOffsetHeight()));
        },

    addOffset: // добавляет смещение к элементу, смещение в формате {x, y}
        function(offsetXY) {
            this.setLocalLeft(parseInt(this.getBaseLeft()) - offsetXY.x);
            this.setLocalTop(parseInt(this.getBaseTop()) - offsetXY.y);
        },

    copyElmRectParameters: // скопировать атрибуты с этого элемента на другой
        function(fromElm, toElm) {
            toElm = toElm || this.element;
            setElmAttr(toElm, DND_BTOP_ATTR, getElmAttr(fromElm, DND_BTOP_ATTR, DND_NS), DND_NS);
            setElmAttr(toElm, DND_BLEFT_ATTR, getElmAttr(fromElm, DND_BLEFT_ATTR, DND_NS), DND_NS);
            setElmAttr(toElm, DND_LTOP_ATTR, getElmAttr(fromElm, DND_LTOP_ATTR, DND_NS), DND_NS);
            setElmAttr(toElm, DND_LLEFT_ATTR, getElmAttr(fromElm, DND_LLEFT_ATTR, DND_NS), DND_NS);
            setElmAttr(toElm, DND_LWIDTH_ATTR, getElmAttr(fromElm, DND_LWIDTH_ATTR, DND_NS), DND_NS);
            setElmAttr(toElm, DND_LHEIGHT_ATTR, getElmAttr(fromElm, DND_LHEIGHT_ATTR, DND_NS), DND_NS);
        }

});

Оба этих класса, надеюсь, помогут вам при решении задач, связанных с опознаванием элементов DOM как графических объектов (например, Drag’n'Drop (здесь я наследовал класс перетаскиваемыx нод, классы областей, их содержащих (несколько с разными свойствами, отнаследованных друг от друга) и помощник для перетаскивания – от GraphicElementWrapper, а главный контейнер – от ElementWrapper) или, например, веб-приложение, эмулирующее работу оконного (здесь, когда я этим занимался, я наследовал перетаскиваемые элементы от GraphicElementWrapper, а меню, статусбар, рабочую область – от ElementWrapper).

Как всё это работает – довольно-таки непростой вопрос, но я постараюсь через некоторое время уделить внимание и ему, возможно в этой же статье… А пока – кажется всё. Удач в JS-конструировании :).

Ссылки

про это…

Наверх