среда, 16 декабря 2009 г.

Планы на будущее

Давно не писал, потому что был несколько нагружен работой и не успевал, а халтуру писать тоже не хочется.
То, о чём я пишу, но ещё не дописал и что, соответственно, в этом блоге будет:
  1. "Дополняем "include в JavaScript`е"". Переработанная статья Володи Колесникова "Инклюд в яваскрипте" на "Техногрете" - сайте для web-разработчиков от студии Артемия Лебедева.
  2. "Хитрость с finally-блоком" - отличный приём кодинга на JS, который, в частности, позволит немного упростить "Базовую модель" приведённую в статьях цикла "Особенности инкапсуляции на основе замыканий".
  3. "Cookies API" - удобный API для работы с Cookies.
  4. "URL advanced parsing" - переработанный в соответстви с серией статей "JavaScript: Особенности инкапсуляции на основе замыканий" вариант реализации парсинга, предложенный Kottenator`ом в статье "Парсим URL")
  5. "Targeter pattern" - придуманный мной приём для динамического создания и подцепки статических блоков контента на странице. Я не раз использовал его в своих проектах и нахожу весьма ценным.
  6. "SPKet IDE - советы". Переработанный вариант статьи и видео-лекции С.Чикуенка "Eclipse: редактирование JavaScript в Spket IDE"
  7. "JSDoc - советы". Дополнения и критика статьи А.Старшинова - "Документирование JS-кода"
  8. "Rhino и scripting API в Java SE 6 - способы применения в Java-программах (на примере JSDoc Toolkit)"
  9. "Универсализированная Обработка событий" - рабор того, что можно сделать с несовместимостью браузеров в области продвинутых моделей обработки событий.
  10. "Универсализированный метод создания DOM-узлов". Представлю свои библиотеки для создания узлов DOM-дерева. Хочу сосредоточиться на нахождении компромиса между максимальной производительностью и удобством работы с API.
  11. "Работа с замыканиями". Распишу, что представляют собой замыкания и как правильно их использовать.
  12. "Примитивы и объекты в JavaScript". Вопреки распространённому мнению, примитивы в JS есть, но они присутствуют в этом языке неявно и преобразуются на лету при попытке обратиться к ним как к объектам. Это может создать путанницу и привести к задержкам в работе кода (что явно и происходит, по моим наблюдениям, в коде у многих). В статье постараюсь подробно изложить отличия одних от других и показать верный путь для тюнинга JS-скриптов в этом отношении.
  13. "Работа с XPath на JavaScript". Описание стандартного механизма поддержки XPath в браузерах и наилучшей с моей т.з.библиотеки, которая заменяет его в браузерах, не поддерживающих данный стандарт.
  14. "Работа с XSLT на JavaScript". Описание работы с библиотекой AJAXSLT от Google.
  15. "Технологии XUL и HTA/HTC". Описание работы с данными технологиями, позволяющими создавать полноценные приложения при помощи Web-технологий.
  16. "ServerSide JavaScript". Когда-то давным-давно, впечатлившись успехом JavaScript, фирма Netscape создала и серверный вариант этого языка, но он по ряду причин не получил должного распространения, упустив рынок приложений на стороне сервера таким языкам, как Perl и PHP. Сейчас, на основе Rhino и Scripting API Java 6 можно использовать Java-сервера для программирования серверной логики на JS. В статье я постараюсь продемонстрировать, как это можно реализовать.
  17. "Технология компонентов". Дальнейшее развитие идеи паттерна Targeter, к которому добавляется аналогичный GUI-библиотекам Java (Swing) механизм обработки событий.

понедельник, 1 июня 2009 г.

JavaScript: Drag and Drop - библиотека

В данной статье я представлю свою небольшую JavaScript-библиотечку для работы с Drag&Drop (Скачать).

Введение

Для начала немного теории. Drag&Drop предполагает, что вы, как разработчик, даёте возможность пользователю перемещать те или иные графические объекты (будем по старинке называть их слоями) по странице, нажав на них левой кнопкой мыши и, не отжимая её, передвигая курсор мыши до пункта назначения.
В более сложном варианте нам необходимо вносить те или иные ограничения на этот процесс, по-этому API библиотеки должно позволять гибко настраивать перенос в соответствии с задачей. Сразу обратим внимание на три фазы перемещения:
  • Нажатие левой кнопки мыши на элементе
  • Перетаскивание элемента с помощью перемещения курсора при нажатой левой кнопке мыши
  • Отжатие левой кнопки мыши после перемещения
На уровне кода данная библиотека представляет собой один объект DnD с двумя методами.

Подключение библиотеки

Архив содержит библиотеку DragAndDrop в 2-х вариантах.
  • Оптимизированный код - в файлах "./lib/common.js" и "./build/dnd.js".
  • Комплексный оптимизированный код - в файле "./dnd.js"
Библиотека "common.js" необходима для ряда операций сравнения, по-этому её обязательно нужно подгружать до подгрузки библиотеки "dnd.js".

Эта библиотека довольно простая и может оказаться полезной вам и для других ваших сценариев - если вы будете её использовать ещё для чего-то (как это делаю я), то имеет смысл её загрузить один раз и больше не загружать - тогда удобно отделить её от других файлов библиотек и загружать отдельно - один раз, что бы потом грузить другие библиотеки, которые её используют (исходный код данной библиотеки лежит там же, в директории "./lib"). Для тестирования (файлы в директории "./test/") выбран именно этот способ, для такой сборки в Ant предназначен target по-умолчанию "build".

Если же вы не собираетесь использовать библиотеку "commons.js" где-либо ещё, то имеет смысл для сокращения количества загружаемых файлов, объединить её с библиотекой dnd в один оптимизированный файл. Такой объединённый файл и лежит в корне архива (он собирается с помощью действия "build-complex" в Ant).

Что ещё есть в архиве

  • "./doc/" - документация
  • "./test/" - демонстрационные тесты
  • "build.xml" - сценарий сборки Ant

Первый пример

Если у Вас на странице есть некоторый объект, который Вам хотелось бы сделать передвигаемым по ней, то Вам достаточно зарегистрировать его в объекте DnD:
var /** @type {HTMLElement} */ layer = document.createElement('div');
layer.appendChild(document.createElement('img')).setAttribute('src', 'pic.gif');
document.body.appendChild(layer);
DnD.register(layer);
В результате выполнения этого кода картинка "pic.gif" появится на экране и станет возможным её передвижение по окну браузера.

Второй метод объекта DnD служит обратной задаче - отмены регистрации. Например, если после пердыдущего участка кода вставить следующий:
setTimeout('DnD.unregister(layer)', 6000);
, то передвижение слоя будет доступно только в течении 6 секунд - затем он зафиксируется и больше не будет доступен для перемещений.

В этом примере мы видим работу двух методов объекта DnD - это методы register и unregister. Как нетрудно догадаться из их названий, один регистрирует объект как DragAndDrop, второй - отменяет эту регистрацию, возвращая его в нормальное состояние (но не отменяя его перемещения, которое уже совершено, а так же текущего перемещения - если метод unregister был вызван в момент перемещения. Если быть более точным, метод unregister блокирует у слоя возможность начать его новое перемещение пользователем). Оба эти метода возвращают ссылки на объект DnD, так что их вызовы можно выстраивать в цепочки любой длинны:
DnD.register(layer1).register(layer2).unregister(layer3).register(layer4);//...
Перемещать можно только слои, у которых свойство 'position' установлено в значение 'absolute' или 'relative'. По-этому при регистрации слоя происходит проверка на то, какое значение выставлено у него в этом css-свойстве. Если оно не выставлено в одно из этих двух - библиотека сама выставляет для него значение 'absolute'.

Установка ограничений

Однако, читатель наверняка согласится с тем, что в реальных задачах редко нужны просто свободно перемещаемые по окну браузера мышкой элементы. Как правило, нужно как-либо ограничить возможности производить перемещения - например, оставить перемещения лишь по заданным траекториям или ограничить те места в браузере, где перемещения можно было бы завершить.

Для этого предусмотрен простой API, регламентирующий Drag&Drop-перемещения для элемента страницы. В основе его лежит событийная модель - для элемента, регистрируемого в объекте DnD, можно определить обработчиков трёх событий. Для каждого из событий в объекте DnD определено поведение по-умолчанию, но если обработчик данного события вернёт булево значение false, то это поведение не будет выполнено. Значение true можно и не возвращать - всё, что угодно, кроме значения false будет командой выполнить действие по-умолчанию (обратите внимание на то, что значение false должно быть иименно примитивным, объект new Boolean(false) будет интерпретирован как true, так же будет интерпретированы и значения null и undefined - только примитив false заблокирует поведение для объекта по-умолчанию).
Таблица №1: Обработчики событий Drag&Drop
СобытиеСигнатура обработчикаОписание параметровПоведение по-умолчанию
Нажатие пользователем левой кнопки мыши в момент нахождения курсора над слоем - инициализация перемещения элемента.function mouseDown(/*Object*/ offset)В качестве парамметра принимает объект offset, содержащий информацию о начале перетаскивания - поля 'x' и 'y' этого объекта содержат разницу между позицией курсора мыши и левым верхним краем элемента, а поле 'startPos' содержит объект с полями 'x' и 'y', содержащими координаты левого верхнего угла элемента.Перемещение стартует. Форма курсора мыши по всему документу выставляется в значение 'move'.
Изменение координат курсора мыши при нажатой левой кнопке мыши.mouseMove(/*number*/ x, /*number*/ y)/*number*/ x - координата по горизонтали, /*number*/ y - координата по вертикали левого верхнего края курсора мыши.Передвижение элемента, по вертикали и горизонтали соответствующее перемещению курсора.
Отпускание левой кнопки мыши после перетаскивания элемента.mouseUp(/*number*/ x, /*number*/ y)/*number*/ x - координата по горизонтали, /*number*/ y - координата по вертикали левого верхнего края курсора мыши.Фиксация слоя в текущей позиции в окне браузера и изменение формы курсора мыши на всём документе на значение 'auto'. В случае отмены данного действия производится возврат слоя к координатам, с которых началось его передвижение.

Можно переопределить любой из обработчиков, все вместе или любые их комбинации - каждый из них будет вызываться только в том случае, если будет присутствовать.

Подробности

Начинается перетаскивание для нас с того, что вызывается метод mouseDown и ему передаётся параметр offset - смещение. Дело в том, что координатой элемента у нас считается координата его верхнего левого угла, а мышка может быть нажата, находясь не только в левой верхней точке, но и в любой другой точке элемента, по-этому для того, что бы корректно перемещать объект в зависимости от будущих координат мыши, нам нужно знать, на сколько пикселов правее и ниже "взялся мышкой" за элемент пользователь, когда начал его перетаскивание, что бы потом корректировать координаты так, что бы курсор мышки при перетаскивании был именно над этой самой точкой, а не над левым верхним углом, как это было бы, если бы мы не учитывали смещения. Именно эту информацию и несёт объект offset. Так же в нём содержится информация о стартовом положении элемента в момент до начала перетаскивания, по-этому может быть удобно запомнить этот объект и использовать эту информацию в дальнейшем - при обработке других событий.

Все три метода могут возвращать значения типа boolean (обратите внимание - с маленькой буквы, т.е. речь идёт о примитиве, а не об обёртке Boolean). Любое другое значение, а так же отсутствие возвращаемого значения интерпретируется как возвращение значения true.

В общем случае возвращение true означает разрешение на проделывание действий по-умолчанию, false - отказ от этих действий. Действия по-умолчанию для событий приведены в таблице №1.

Пример: Перетаскивание только по горизонтали

Допустим, перед нами стоит задача создать интерфейс, управляющий какой-либо количественной величиной, скажем, уровнем громкости в динамике или задержкой по времени при автодозвоне. Тогда нам нужно создать некоторый бегунок, который бы перемещался только по горизонтали, по нужной нам шкале, а мы бы отслеживали это перемещение и, в соответствии с ним, меняли бы ту величину, за изменение которой был бы ответственнен наш компонент.

На уровне API это означает, что нам нужно заблокировать поведение по-умолчанию при возникновении события mouseMove и заменить его на перемещение по горизонтали, не трогая вертикальной координаты объекта.

Делается это примерно так:
(function() {
    var /** @type {number} */ offsetX;

    layer.mouseDown = function(offset) {
        offsetX = offset.x;
    }

    layer.mouseMove = function(x) {
        this.style.left = (x - offsetX) + 'px';
        return false;
    }

    layer.mouseUp = function() {
        offsetX = null;
    }
})();
Вставьте этот код перед регистрацией слоя в объекте DnD в первом примере (в начале статьи) - и он придаст слою нужную функциональность. Послоностью пример можно увидеть, посмотре исходные коды тестов (директория "./test/") - там этот пример соответствует примеру №3. Запустите файл "./test/test.html" и проверьте его работу в своём браузере.

Пример: перетаскивание в заданную область

Допустим, у нас есть интернет-магазин и мы хотим сделать возможность перемещения товаров в корзину. Тогда мы имеем задачу обеспечить перетаскивание элемента в ограниченную область экрана (корзину) и нас не устроит, если пользователь переместит слой, соответствуюдий товару, куда-либо ещё.

Значит, нам нужно перегрузить обработчик события окончания передвижения элемента с тем, что бы проверять, находится ли курсор над областью, в которую разрешено перемещение и, в случае, если нет - возвращать false с тем, что бы вернуть элемент назад, отменив результат перемещения.

Кроме того, было бы полезно в процессе перемещения сразу показывать пользователю, что в данный момент элемент находится не над приемлемой для помещения в неё зоной. Традиционно это делается путём присвоения курсору такого значения, которое выглядит как знак запрета. К сожалению, такое значение для CSS-свойства 'cursor' на сегодняшний момент не стандартизовано, однако, несмотря на это, поддерживается последними версиями всех достаточно популярных браузеров, за исключением, разве что, Opera`ы (будем надеяться, что это временно). Это значение - 'no-drop'.

Допустим, что область карзины представлена объектом SquareArea, которому в конструкторе передаются её координаты и размеры, и у неё есть один метод - isInside(/*number*/ x, /*number*/ y), который возвращает результат типа boolean, говорящий, соответственно, входит или не входит точка с переданными ему координатами в его область. Тогда наш код для этой ситуации может быть примерно таким:
(function() {

    var /** @type {Object} */ offset,
        /** @type{SquareArea} */ toArea = new SquareArea(450, 450, 100, 100);

    layer.mouseDown = function(_offset) {
        offset = _offset;
    }

    layer.mouseMove = function(x, y) {

        this.style.cursor =
        document.body.style.cursor = toArea.isInside(x, y) ? 'move' : 'no-drop';
    }

    layer.mouseUp = function(x, y) {

        offset = null;
        this.style.cursor = 'move';

        return toArea.isInside(x, y);
    }
})();
Наверное, нужно особо остановиться на том, почему в обработчике mouseMove значение курсора присваивается сразу же двум стилям элементов - и самого элемента и документа. Дело в том, что при перемещении, резко дёрнув мышь, можно на долю секунды поймать момент, когда элемент ещё не успел перерисоваться и подтянуться к курсору мыши - и тогда, если документу не присвоен тот же курсор, что и элементу, курсор изменит форму, а потом вернёт старую форму обратно. Это может смотреться довольно раздражающе - именно для того, что бы избежать этого эффекта, рекомендуется в процессе DragAndDrop-передвижения выставлять необходимую форму курсора не только над элементом, но и на всём документе.

Вставьте этот код перед регистрацией слоя в объекте DnD в первом примере (в начале статьи) - и он придаст слою нужную функциональность. Полоностью пример можно увидеть, посмотре исходные коды тестов (директория "./test/") - там этот пример соответствует примеру №4. Запустите файл "./test/test.html" и проверьте его работу в своём браузере.

вторник, 28 апреля 2009 г.

JavaScript: Реализация pattern`а Singleton

По моему мнению гибкость языка JavaScript оставляет далеко позади практически все известные мне языки. Одним из тонких моментов является работа оператора new. Мы привыкли, что он создаёт объект с помощью конструктора, передавая ему аргументы. Но на самом деле результат может быть и иным: если в функции, используемой в качестве конструктора, вернуть какой-либо другой объект, то он-то и станет результатом всего выражения с оператором new. Посмотрим на пример:
function X(){}
function Y(){ return new X; }
var y = new Y;
alert(y instanceof Y); // false
alert(y instanceof X); // true
Этот приём, например, можно очень удобно использовать для того, что бы заставить IE работать, как это должен делать любой нормальный браузер, с объектом XMLHttpRequest без всяких заморочек с его ActiveX`овскими конструкторами:
// Provide the XMLHttpRequest class for IE 5.x-6.x:
if (typeof XMLHttpRequest == "undefined")
    /** @constructor */
    XMLHttpRequest = function() {
        try { return new ActiveXObject("Msxml2.XMLHTTP.6.0") } catch(e) {}
        try { return new ActiveXObject("Msxml2.XMLHTTP.3.0") } catch(e) {}
        try { return new ActiveXObject("Msxml2.XMLHTTP") } catch(e) {}
        try { return new ActiveXObject("Microsoft.XMLHTTP") } catch(e) {}
        throw new TypeError( "This browser does not support XMLHttpRequest." )
    };
- после этого можно работать с этим конструктором практически так же, как во всех остальных браузерах - FireFox`е, Safari, Opera`е и Chrom`е. Этот приём описан в Wikipedia.

Обычно для реализации Singleton`а применяются специальные конструкции на основе методов и прочего. Но в JavaScript представляется намного более простым использование этой возможности так, что бы внешний интерфейс создания экземпляра вообще не отличался от возврата уже созданного предыдущими вызовами экземпляра.
Так может быть реализован и pattern "Singleton" на JavaScript:
/**
 * @constructor
 * @singleton
 */
function SingletonClass() {

    if (arguments.callee.instance)
        return arguments.callee.instance;
   
    //...
   
    return arguments.callee.instance = this;
}


Update №1 (18.01.2010)
Мне пришла в голову мысль о том, что не очень хорошо делать так, как я предложил в примере выше. Дело в том, что поле instance открыто для редактирования и внесения изменений и этот механизм из-за этого можно сломать. Конечно, вряд ли кто-то станет намеренно так поступать, но лучше всё-таки не оставлять такой возможности.
Так что сейчас я оформляю Singleton`ы несколько подругому:
/**
 * @constructor
 * @singleton
 */
var SingletonConstructor;
(function() {
    var /** @type {SingletonConstructor} */ instance;
    SingletonConstructor = function() {
        if (typeof instance !== 'undefined')
            return instance;
       
        //...
       
        return instance = this;
    };
})();

четверг, 23 апреля 2009 г.

JavaScript: Выяснение имени класса (конструктора)

В статье "JavaScript: используйте arguments.callee вместо названия функции" я уже обращал ваше внимание на то, что в JavaScript в общем случае нельзя полагаться на то, что имена функций будут соответствовать тем функциям, которым вы их присвоили. Вещь это настолько важная, что не грех будет кратко повторить суть рассуждений.

В JavaScript функция является одним из типов данных, специфика которого заключается в том, что к нему можно обращаться, вызывая его как функцию и как конструктор. Но если это тип данных, то, что бы с ним работать, нам нужно получить ссылку на него, которая может содержатся в какой-то переменной. Это и происходит при объявлении функции, по сути имя функции - это переменная, которой присваивается значение - функция. В этом смысле следующие две строчки эквивалентны:
function x(){}
var x = function(){}
Между этими строками есть небольшое отличие, но в данном случае оно не существенно. Так вот, само название ПЕРЕМЕННАЯ говорит нам о том, что она может менять своё значение, по-этому опираться на то, что эта переменная будет всегда ссылаться на один раз присвоенную ей функцию, нельзя. Следующий пример это демонстрирует:
function x(){};
//...
x = 5;
//...
x(); //Error!
Но что же делать, если про функцию всё-таки нужно узнать её имя? Например, у нас может быть написан следующий код:
function x1(){}
var x2;
if (x1) x2 = x1;
x1 = 5;
В результате ссылка на функцию есть, но как её найти, имея лишь саму функцию - не понятно.
В некотором частном случае, а именно если объявление функции и манипуляции с ней происходили в глобальном контексте (глобальной области видимости переменных), выяснение имени возможно.

Здесь следует сказать пару слов о глобальном контексте. В JavaScript определён объект Global, доступный по ссылке this. Это означает, что если мы объявляем переменную не внутри какой-либо функции, а просто в сценарии, то она автоматически становится свойством объекта Global и к ней можно обращаться не только по имени, но и как к свойству этого объекта:
var x = 5;
alert(this.x); // '5'
В частном, но наиболее часто встречающемся случае клиентского JavaScript (client-side JavaScript), т.е. расширения стандарта ECMAScript в браузерах моделью DOM, нам доступна ссылка window, укзывающая на объект Window, который является расширением объекта Global для браузерной среды, и в нём, соответственно, так же можно, обращаться к его свойствам как к переменным, однако в общем случае использование ссылки window нельзя назвать корректным, по крайней мере лично мне хотелоось бы писать решения, которые работали бы, кроме клиентского JavaScript, ещё и, скажем, в Java-программах, использующих Scripting API, поддержка которого есть в JDK6.

Со ссылкой же на объект Global есть некоторая сложность, связанная с тем, что она по сути и доступна будет лишь в глобальном контексте и в функциях, которые не являются свойствами различных объектов, поскольку в других местах ссылка this будет ссылаться на другие объекты. Что бы решить эту проблему, предлагаю ввести специальную переменную global, которая будет ссылаться на глобальный контекст:
var /** @type {Global} */ global = this;
Теперь нам будет проще написать нужную функцию, но куда её лучше всего разместить? Мне представляется логичным помещение её в Function.prototype, поскольку тогда она станет доступна у всех функций (т.к. любая функция представляет собой экземпляр объекта Function и ссылается по-этому своей ссылкой __proto__ на объект Function.prototype).

Итак, какой код получаем в результате:
var /** @type {Global} */ global = this;

//...

/**
* Функция, возвращающая имя функции this, если оно присутствует в глобальном
* контексте.
*
* @return {string} имя функии, доступное по ссылке this или null, если имя не
* найдено.
*/
Function.prototype.getName = function() {

    for (var /** @type {string} */ i in global)
        if (global[i] === this)
            return i;

    return null;
}

//...


//Тестируем:

//Объект узнаёт имя своего конструктора:
function x1(){
    this.showMyName = function() {
        alert('My constructor name is ' + this.constructor.getName());
    }
}
y = new x1;
y.showMyName(); // 'My constructor name is x1'

var x2;
if (x1) x2 = x1;
x1 = 5;
y.showMyName(); // 'My constructor name is x2'

// Функция узнаёт своё имя:
function x3(){ alert('My name is ' + arguments.callee.getName()); }
x3(); // 'My name is x3'

// Узнаём у функции её имя:
function x4(){}
alert(x4.getName()); // 'x4'
Таким образом, функция, если ссылка на неё присутствует в глобальном контексте, всегда сможет узнать своё имя.

Update: К сожалению, в последней на сегодняшний день версии Internet Explorer`а - 8 (тестировал на версии 8.0.7600.16385) - всем переменным глобального пространства имён и, соответственно, функциям, автоматически присваивается модификатор {Don`t Enum}, из-за чего данный метод в этом браузере не работает... :(

понедельник, 20 апреля 2009 г.

JavaScript: особенности инкапсуляции на основе замыканий. Часть 4: Наследование

Существует огромное множество различных реализаций наследования в JavaScript - от кустарных приёмов до реализаций в раскрученных библиотеках типа JQuery и Prototype. Но всех их объединяет один недостаток - они не позволяют использовать инкапсуляцию на основе замыканий (и никакую другую тоже). По крайней мере мне не попадалось ни одной реализации наследования в JS, которая позволяла бы скрывать данные.

В предыдущих статьях данного цикла (1, 2, 3 части) мной была пошагово продемонстрирована разработка "базовой модели", которая позволяет реализовать структуру, предоставляющую возможность экономно создавать экземпляры классов сложных объектов, разруливая проблему совмещения инкапсуляции с ООП на основе прототипов. Конечно, само по себе решение этой проблемы довольно-таки важно, но истинную мощь "базовая модель" получит только тогда, когда будет дополнена механизмами наследования, поскольку одноуровневые объектные структуры весьма ограничены в рамках дизайна больших приложений (на которые и нацелена "базовая модель").

Итак, давайте разберёмся, что реально означает наследование одного класса от другого на уровне работы кода?
Если смотреть извне, то это означает, что любой код, нормально работающий с объектом унаследованного класса, должен продолжать нормально работать с объектом его потомка. Т.е. у объекта потомка присутствуют все методы объекта родителя и они внешне (т.е. на уровне типов возвращаемых значений, принимаемых параметров и возбуждаемых исключений) ведут себя так же.
Если же смотреть на наследование изнутри, то наследник:
  1. Должен в конструкторе первым делом вызывать конструктор класса-предка.
  2. Всегда иметь ссылку на экземпляр созданного в конструкторе класса-предка в переменной super.
  3. Некоторые public-методы могут быть переопределены и добавлены новые. То же самое в отношении public-полей.
  4. Существует возможность открывать потомкам доступ к некоторым функциям и полям (модификатор protected в Java).
  5. Только те исключения (и их потомков), которые мог вызывать переопределённый метод, может вызывать метод переопределяющий. Либо последний исключений может не вызывать вовсе.
По поводу 2 пункта замечу, что слово super в JavaScript является зарезервированным и поэтому его использовать не получится, довольствуясь лишь _super, что вобщем-то даже имеет положительную сторону - будет сразу восприниматься в качестве особой переменной.
Ну а по поводу 4 пункта, честно признаюсь, сколько ни думал о том, как это реализовать с применением замыканий, так ничего путного и не надумал. Возможно, я придумаю, как можно хитро вывернуться, всё-таки реализовав это, но пока давайте про этот пункт забудем и реализуем наследование без него, считая, что такого модификатора у нас нет.

Принципиально важным, на мой взгляд, является возможность реализации наследования так, что бы не замыкать её только на объекты, созданные в соответствии с базовой моделью, а дать возможность использовать в отношении любых конструкторов пользовательских объектов.

Итак, как нам нужно теперь модифицировать наш код, что бы стало возможным использование наследования? Для начала давайте посмотрим на схему:
Схема наследования для Расширенной модели
Мы видим, что прототип ссылается неявной ссылкой (доступной в FireFox по имени __proto__ и недоступной в других браузерах - по-этому я называл её здесь именно так) на объект-прототип класса-предка, а на экземпляр ссылается только ссылка _super.

Первое изменение коснётся механизма создания прототипа. Т.к. разработчики спецификации ECMAScript не оставили нам возможности напрямую контролировать ссылку __proto__, мы вынуждены хитрить, что бы заставить её ссылаться на то, что нам будет нужно.

В Базовой модели конструктор прототипа выглядел так:
A.prototype = new function() {/*здесь код конструктора прототипа*/}
A.prototype.constructor = A;
Таким образом, у нас создавалась безымянная (анонимная) функция и тут же использовалась в качестве конструктора нового объекта-прототипа для класса A. Затем, что бы не нарушать принятые в JavaScript договорённости относительно взаимных ссылок между конструкторами и прототипами, мы выставляли свойству constructor прототипа ссылку на класс.

Теперь же, учитывая, что мы не можем напрямую контролировать ссылку __proto__, единственная возможность для нас прописать в неё то значение, которое нам нужно, состоит в том, что бы функция, создающая объект, который должен ссылаться посредством __proto__ туда, куда нам надо, должна до создания объекта ссылаться туда свойством prototype:
function X() {}
function Y() {this.z = 5;}
var x1 = new X;
X.prototype = new Y;
var x2 = new X;
alert(x1.z); //Выведет 'undefined'
alert(x2.z); //Выведет '5';
Т.е. получается, что функция, создающая для нас прототип, теперь не сможет быть безымянной. А как не хочется засорять пространство имён ещё одной переменной... По-этому я предлагаю немного схитрить - присвоить прототипу сначала эту функцию, а потом, произведя все нужные изменения, присвоить ему уже её результат. Выглядеть это будет вот так:
A.prototype = function() {/*...*/}
A.prototype.prototype = B.prototype; // B - конструктор, от которого мы наследуем A
A.prototype = new A.prototype;
A.prototype.constructor = A;
На выходе получим прототип, ссылающийся ссылкой __proto__ на прототип конструктора-предка.

Теперь осталось разобраться со ссылкой _super. Она будет обладать похожим поведением на ссылку _this - так же будет переменной, имеющий двойника - переменную __super, являющуюся полем объекта. Так же, как и ссылку __this, её не будет смысла очищать в методе privateState.get() и, соответственно, наполнять в privateState.set(). Так же она будет константой для пользователя.
Кроме того, она будет присваиваться вызову конструктора-предка вначале метода pcreatePivateState и в нём же из неё будут извлекаться все свойства и присваиваться ссылке __this - если они, конечно, не будут переопределены в классе-потомке.

Теперь соберём всё вместе и протестируем. Вот, какая получается расширенная модель:
function B() {
    var y = 15;

    this.getY = function(){
        return y;
    }

    this.setY = function(_y) {
        y = _y;
        return this;
    }
}

/**
* @author Vyacheslav Lapin
* @version 0.01 09.04.2009 21:10:04
*
* @constructor
* @extends B
*
*
* @param {number} x +
*/
function A(x) {

    if (this.constructor !== arguments.callee)
        return new arguments.callee(x);

    this.privateState = this.createPrivateState(x);
}

A.prototype = function() {

    var /** @type {A} */ _this,
        /** @type {B} */ _super,

        /**
         * @static
         * @type {Array<A>}
         */
         __callInstances = [];

    this.createPrivateState = function(__x) {

        //Переменные для хранения private-полей объекта
        var /** @type {number} */ _x = __x,

            /**
             * @constant
             * @type {A}
             */
            __this = this,

            /**
             * @constant
             * @type {B}
             */
            __super = new B(); // Вызываем конструктор класса-предка

        this.createPrivateState = null;

        // Отдельный функциональный блок для того, что бы не заводить в
        // closure ещё одно поле - переменную i
        (function() {
            //Здесь мы перечисляем свойства экземпляра класса-предка
            for (var /** @type {string} */ i in __super)
                // Проверяем, собственное это свойство экземпляра super-класса
                // или унаследованное? Ведь унаследованные свойства нам не нужны -
                // мы и так их унаследуем по цепочке прототипов, если они не будут
                // переопределены в нашем прототипе, а тогда - унаследуем
                // переопределённые, что нас вполне устроит.
                if (__super.hasOwnProperty(i)
                    // Так же проверяем, есть ли такие же в прототипе
                    && !this.constructor.prototype.hasOwnProperty(i)
                    // или в самом объекте?
                    && !this.hasOwnProperty(i)
                )
                    try {
                        this[i] = __super[i];
                    } catch (/** @type {Error} */ e) { }
        })();

        return {
            get: function() {

                if (_this) {
                    __callInstances.push(_this);
                    _this.privateState.set();
                }

                x = _x;
                //...
                _this = __this;
                _super = __super;

                _x =
                //...
                null;
            },

            set: function() {

                _x = x;
                //...

                x =
                //...
                _this =
                _super = null;

                if (__callInstances.length
                    && __callInstances[__callInstances.length - 1] !== __this)

                    __callInstances.pop().privateState.get();
            }
        }
    }

    /**
     * @param {Arguments} args +
     */
    function methodCaller(args) {
        this.privateState.get();
        var result = args.callee.apply(_this, args);
        this.privateState.set();
        return result;
    }

    //--------------------------------------------------------------------------

    var /** @type {number} */ x;
    //...

    /**
     * @return {number}
     */
    this.getX = function() {

        if (_this !== this) return methodCaller.call(this, arguments);

        return x;
    }

    /**
     * @param {number} _x
     * @return {A} this
     */
    this.setX = function(_x) {

        if (_this !== this) return methodCaller.call(this, arguments);

        x = _x;

        return this;
    }

    /**
     * @override
     */
    this.setY = function(_y) {

        if (_this !== this) return methodCaller.call(this, arguments);

        alert('Hello from A!');
        _super.setY(_y);
       
        return this;
    }

}

/** @type {string} */ A._className = "A";
A.prototype.prototype = B.prototype;
A.prototype = new A.prototype;
A.prototype.constructor = A;

// Импортируем в конструктор-потомок так же статические поля
// конструктора-предка- свойства класса
(function() {
    for (var /** @type{string} */ i in B)
        if (!(i in A))
            try {
                A[i] = B[i];
            } catch (/** @type {Error} */ e) { }
})();

//test
var a = new A(10);
alert(a.getY()); //Выводит '15'
alert(a.setY(20).getX()); // Выводит сначала 'Hello from A!', затем 10
alert(a.getY()); //Выводит '20'
Вот и всё. Можно теперь делать любые сложные деревья наследования в JavaScript в полном соответствии с принципами инкапсуляции.

В дальнейших статьях цикла мы поговорим о том, как упростить разработку конструкторов по базовой и расширенной модели, поскольку у меня нет иллюзий о том, что вручную писать такой код не слишком-то удобно.

P.S. Напоследок хочу предостеречь вас от попытки на радостях наследовать от системных конструкторов - к сожалению, с ними может не пройти выражение:
__this[i] = __super[i];
Дело в том, что они не являются в полном смысле JavaScript`овыми объектами и для них такой возможности часто не предусмотрено. Например, мне не удалось наследовать от XMLHttpRequest - там это выражение вылетало с ошибкой даже при том, что я обромил его блоком try/catch:
try {__this[i] = __super[i];} catch(/** @type {Error} */ e) {}
Всё равно, даже такой бронебойный код вываливался с ошибкой в браузере. По крайней мере в FF это дело у меня не прошло. Так что ещё раз повторюсь - расширенная модель предназначена для наследования от родных JavaScript-конструкторов объектов. Будьте осторожны!

Другие части:
Часть 1: Основа Базовой модели
Часть 2:Механизм вызова методов
Часть 3: Внутренние вызовы

JavaScript: Контроль типов

JavaScript считается слабо-типизированным зыком не потому, что у него нету типов, а потому, что у переменных в этом языке не фиксируется, на объект или примитив какого типа они будут смотреть. При этом интерпретатор JS преобразует типы в зависимости от контекста их использования.

Тем не менее, не всегда это удобно. В частности, это не удобно, когда пишешь библиотеку для кого-то, кто потом может вызвать твои методы не так. По-этому я иногда предпочитаю вносить дополнительный код в свои скрипты, что бы чётко определять, какими типами должны являться значения тех или иных переменных.

Это решение направлено не только на проверку, но ещё и на преобразование типа к тому, которого я жду во избежание разных фокусов, которые могут подстерегать при автоматических контекстных преобразованиях.

В интегрированной среде разработки Eclipse есть такое средство, как Snippets, прекрасно подходящее для решения задачи контроля типов в JavaScript. Snippet - это кусочек кода в общем виде, настраиваемый под конкретную ситуацию путём редактирования некоторых имён и значений, называемых переменными snippet`ов. Их можно создавать и группировать, что бы потом удобно было находить и использовать. Я у себя создал группу "Params check" и в ней расположил snippet`ы для всех примитивов.

Перед тем, как начать, следует ввести константы-обозначения типов, с которыми будет проще осуществлять эти проверки. Эти константы будут иметь строковые типы и будут служить для сравнения результатов операции typeof, применяемой к переменным:
var undefined,
    Types = {
        // Primitive types
        /** @type {string} */ STRING_TYPE: 'string',
        /** @type {string} */ OBJECT_TYPE: 'object',
        /** @type {string} */ NUMBER_TYPE: 'number',
        /** @type {string} */ BOOLEAN_TYPE: 'boolean',
        /** @type {string} */ FUNCTION_TYPE: 'function',
        /** @type {string} */ UNDEFINED_TYPE: 'undefined'
    };
Итак, начнём с самого простого типа - boolean. На данный момент я произвожу проверку на него следующим блоком кода:
//Param x as boolean check
if (typeof x !== Types.BOOLEAN_TYPE)
    if (x instanceof Boolean)
        x = x.valueOf();
    else {
        if (x instanceof String)
            x = x.valueOf();

        if (typeof x === Types.STRING_TYPE)
            x = x !== '' && x !== 'false';
        else
            x = x ? true : false;
    }
, изначальный же Snippet для этогой проверки должен содержать переменную snippet`а для имяни переменной, что бы внести его один раз и больше не маяться:
//Param ${varName} as boolean check
if (typeof ${varName} !== Types.BOOLEAN_TYPE)
    if (${varName} instanceof Boolean)
        ${varName} = ${varName}.valueOf();
    else {
        if (${varName} instanceof String)
            ${varName} = ${varName}.valueOf();

        if (typeof ${varName} === Types.STRING_TYPE)
            ${varName} = ${varName} !== '' && ${varName} !== 'false';
        else
            ${varName} = ${varName} ? true : false;
    }
Следующий пример - для наиболее частого в использовании типа - строк:
//Param s1 as string check
if (s1 === null || typeof s1 === Types.UNDEFINED_TYPE)
    throw new TypeError(
        'Required param - s1 - is not specified by function call!'
    );

if (typeof s1 !== Types.STRING_TYPE)
    s1 = s1 instanceof String ?
            s1.valueOf()
            : s1.toString();

if (!/^[1-0a-f]*$/i.test())
    throw SyntaxError('Required param - s1 - has invalid format.');
Обратите внимание, что здесь у нас уже две переменных для строки я счёл важным не только проверку её типа, но ещё и правильную проверку её формата с использованием регулярного выражения. В данном примере по шаблону проверяется, является ли строка представлением числа в 16-ричном формате.
Теперь решение в общем виде - string Snippet:
//Param ${varName} as string check
if (${varName} === null || typeof ${varName} === Types.UNDEFINED_TYPE)
    throw new TypeError(
        'Required param - ${varName} - is not specified by function call!'
    );

if (typeof ${varName} !== Types.STRING_TYPE)
    ${varName} = ${varName} instanceof String ?
            ${varName}.valueOf()
            : ${varName}.toString();

if (!/^${regexp}$$/.test())
    throw SyntaxError('Required param - ${varName} - has invalid format.');
Что же касается числового типа - number, то здесь имеется небольшой подводный камешек. Дело в том, что целые и вещественные числа спецификация ECMAScript не различает. Однако, как правило, в сценарии по логике легко понять, какого именно числа мы ждём - целого или дробного, так что я сделал два разных блока проверки - для целых (integer) и вещественных (float) чисел:
//Param x as integer check
if (x === null || typeof x === Types.UNDEFINED_TYPE)
    throw new TypeError(
        'Required param - x - is not specified by function call.'
    );

if (typeof x !== Types.NUMBER_TYPE)
    if (x instanceof Number)
        x = x.valueOf();
    else {
        if (x instanceof String)
            x = x.valueOf();

        if (typeof x === Types.STRING_TYPE)
            x = parseInt(x);
        else
            throw new TypeError(
                'Required param - x - has invalid type!'
            );
    }

if (x < 1 || x > 100)
    throw new RangeError(
        'Required param - x = '
        + x
        + ' - is out of correct range (1 - 100).'
    );


//Param y as float check
if (y === null || typeof y === Types.UNDEFINED_TYPE)
    throw new TypeError(
        'Required param - y - is not specified by function call.'
    );

if (typeof y !== Types.NUMBER_TYPE)
    if (y instanceof Number)
        y = y.valueOf();
    else {
        if (y instanceof String)
            y = y.valueOf();

        if (typeof y === Types.STRING_TYPE)
            y = parseFloat(y);
        else
            throw new TypeError(
                'Required param - y - has invalid type.'
            );
    }

if (y < 0.55 || y > 0.99)
    throw new RangeError(
        'Required param - y = '
        + y
        + ' - is out of correct range (0.55 - 0.99).'
    );
Видим, что для чисел кроме типа проверяется тот интервал значений, в который они должны попадать. Если они не входят в него, возбуждается исключение. Snippet`ы:
//Param ${varName} as integer check
if (${varName} === null || typeof ${varName} === Types.UNDEFINED_TYPE)
    throw new TypeError(
        'Required param - ${varName} - is not specified by function call.'
    );

if (typeof ${varName} !== Types.NUMBER_TYPE)
    if (${varName} instanceof Number)
        ${varName} = ${varName}.valueOf();
    else {
        if (${varName} instanceof String)
            ${varName} = ${varName}.valueOf();

        if (typeof ${varName} === Types.STRING_TYPE)
            ${varName} = parseInt(${varName});
        else
            throw new TypeError(
                'Required param - ${varName} - has invalid type!'
            );
    }

if (${varName} < ${min_value} || ${varName} > ${max_value})
    throw new RangeError(
        'Required param - ${varName} = '
        + ${varName}
        + ' - is out of correct range (${min_value} - ${max_value}).'
    );


//Param ${varName} as float check
if (${varName} === null || typeof ${varName} === Types.UNDEFINED_TYPE)
    throw new TypeError(
        'Required param - ${varName} - is not specified by function call.'
    );

if (typeof ${varName} !== Types.NUMBER_TYPE)
    if (${varName} instanceof Number)
        ${varName} = ${varName}.valueOf();
    else {
        if (${varName} instanceof String)
            ${varName} = ${varName}.valueOf();

        if (typeof ${varName} === Types.STRING_TYPE)
            ${varName} = parseFloat(${varName});
        else
            throw new TypeError(
                'Required param - ${varName} - has invalid type.'
            );
    }

if (${varName} < ${min_value} || ${varName} > ${max_value})
    throw new RangeError(
        'Required param - ${varName} = '
        + ${varName}
        + ' - is out of correct range (${min_value} - ${max_value}).'
    );
Вот и всё. Остаётся лишь добавить, что чаще всего я использую такие проверки для написания внешних библиотек, что бы контролировать те значения, которые приходят в мою функцию. Эти проверки излишни, если тот код, который вы пишете, будет вызываться так же вами и, таким образом, легче установить контроль с вызывающей стороны. Однако бывают ситуации, когда параметры вызова так или иначе зависит не от вас и тогда такие проверки бывают оправданны.

воскресенье, 19 апреля 2009 г.

JavaScript: особенности инкапсуляции на основе замыканий. Часть 3: Внутренние вызовы

Продолжаю серию публикаций, направленную на усовершенствование применения инкапсуляции на основе замыканий. В первой статье, "Базовая модель", я поставил проблему несовместимости инкапсуляции на основе замыканий с реализацией ООП на основе прототипов, каковая наличествует в JavaScript и предложил т.н. "базовую модель" того, как можно было бы это обойти. Во второй статье цикла, "Механизм вызова методов", я сосредоточил внимание на упрощении вызова методов в рамках базовой модели и исправлении недочёта базовой модели в отношении использования ключевого слова this и в отношении вызовов объектом собственных методов. Данная статья посвящена окончанию разработки базовой модели. Осталась ещё не решённой проблема "внутренних вызовов", т.е. вызовов одним экземпляром объекта методов другого экземпляра этого же объекта. Итак, какая проблема возникает в базовой модели при внутреннем вызове? Давайте посмотрим по-внимательнее на методы get и set:
get: function() {
    x = _x;
    _this = __this;
    _x = null;
},
set: function() {
    _x = x;
    x =
    _this = null;
}
, а так же давайте снова взглянем на methodCaller:
function methodCaller(args) {
    this.privateState.get();
    var result = args.callee.apply(_this, args);
    this.privateState.set();
    return result;
}
Итак, что у нас получится, когда объект вызовет какой-нибудь метод другого объекта, являющегося экземпляром того же самого класса? Очевидно, у вызываемого объекта выполнится метод privateState.get(), который затрёт значения переменных вызывающего объекта и они окажутся безвозвратно утеряны. Наилучшим решением представляется модификация методов get и set так, что бы первый проверял, наполнен ли прототип в данный момент переменными какого-либо объекта или пуст перед тем, как вносить изменения в его переменные и сохранял их куда-то, а второй, после очищения прототипа проверял, были ли сохранены данные предыдущего объекта и возвращал бы в таком случае эти значения на место. Тогда сначала надо придумать, как определить - наполнен ли объект или пуст. Здесь нам опять поможет специальная переменная _this, в отношении которой мы в прошлой статье договорились, что она будет ссылаться на объект, переменными которого в данный момент наполнен прототип и очищаться в методе set, где ей будет присваиваться значение null:
get: function() {
    if (_this) //Объект уже чем-то наполнен
    //...
}
Ну хорошо, выяснили - наполнен, что делать дальше? Очевидно, нужно куда-то сохранить значения переменных, что бы потом, в методе set, присвоить их обратно. Но куда и как? Перечислять снова все переменные вручную - это уже окончательно перегрузит "базовую модель". Наверное, лучше всего просто вернуть переменные тому объекту, который их разместил, вызвав у его privateState метод set, с тем, что бы потом, в методе set вернуть их назад методом privateState.get. Примерно так:
var __instance = null;
return {
    get: function() {
        if (_this)
            (__instance = _this).privateState.set();
        x = _x;
        _this = __this;
        _x = null;
    },
    set: function() {
        _x = x;
        x =
        _this = null;
        if (__instance) {
            __instance.privateState.get();
            __instance = null;
        }
    }
}
Вроде бы, всё нормально. Но... теоретически, возможна ситуация, когда этого будет не достаточно. Представьте ситуацию, когда a1 и a2 являются объектами, созданными конструктором A и, соответственно, ссылаются на один и тот же прототип. Тогда давайте проанализируем, что же будет происходить:
  1. Какой-то метод a1 вызывает другой или тот же самый метод a2.
  2. Тогда метод a2.privateState.get() вызовет a1.privateState.set(), очищая прототип от переменных a1 и наполняя его своими переменными.
  3. После выполнения вызванного метода a2 вернёт всё обратно, вызвав в методе a2.privateState.set() метод a1.privateState.get().
  4. Метод объекта a1 продолжит выполнение, как ни в чём не бывало. В распоряжении прототипа в этот момент будут именно переменные a1.
Теперь давайте усложним ситуацию - введём дополнительно объект a3:
  1. Какой-то метод a1 вызывает другой или тот же самый метод a2.
  2. Тогда метод a2.privateState.get() вызовет a1.privateState.set(), очищая прототип от переменных a1 и наполняя его значениями своих полей.
  3. Выполняющийся в данный момент метод объекта a2 вызывает какой-то из методов объекта a3.
  4. Вызывается метод a3.privateState.get(), который вызывает метод a2.privateState.set(), который в свою очередь очищает прототип от переменных объекта a2 и (что нам вовсе не нужно) вызывает a1.privateState.get(), который прописывает в прототип значения переменных a1.
  5. После выполнения a2.privateState.set(), метод a3.privateState.get() продолжит выполняться и затрёт в прототипе все переменные объекта a1, присвоив им значения переменных a3. Объект a1 потеряет свои поля.
Таким образом, более сложная схема взаимодействия объектов, созданных одним конструктором, уже перестанет корректно работать. Решением данной проблемы представляется создание не одной переменной, а массива объектов и хранение его не в privateState, а в прототипе, как закрытую статическую переменную. В этот массив можно будет помещать объекты данного класса, которые находятся на более высоких, чем текущий, уровнях "лестницы вызова":
var /** @static
     * @type Array<Object> */
    __callInstances = [];
Тогда нам так же понадобится разделить функциональность метода privateState.set() - что бы выкатывать состояние предыдущего объекта в случае вызова этого метода methodCaller`ом и не выкатывать - в случае вызова privateState.get`ом другого экземпляра того же класса. Определить это проще всего, договорившись добавлять объект в методе privateState.get() в массив __callInstances прежде, чем вызывать у него метод privateState.set() - тогда по нахождению объекта __this в конце массива мы сможем со всей определённостью понять - вызывают метод privateState.set() из privateState.get()`а другого экземпляра того же класса (и тогда выкатывать переменные последнего объекта в массиве __callInstances не нужно) или он вызывается из methodCaller`а прототипа (и тогда, соответственно, нужно):
get: function() {
    if (_this) {
        __callInstances.push(_this);
        _this.privateState.set();
    }
    x = _x;
    _this = __this;
    _x = null;
},
set: function() {
    _x = x;
    x =
    _this = null;
    if (__callInstances.length
        && __callInstances[__callInstances.length - 1] !== __this)
        __callInstances.pop().privateState.get();
}
Так же изменения немного коснутся и механизма вызова methodCaller`а. Напомним, каким он у нас стал после выполнения действий из предыдущей статьи цикла:
if (_this) return methodCaller.call(this, arguments);
Теперь для того, что бы выполнять или не выполнять его, нам недостаточно будет знать о простом наличии в переменной _this какого-то не null`евого значения - ибо в случае вызова одним экземпляром метода другого экземпляра того же класса, у нас переменная _this будет иметь значение, но оно будет не верным, поскольку будет ссылаться на вызывающий, а не на вызываемый объект. Так что теперь нам нужно будет в этом случае осуществлять проверку на идентичность значения этого свойства переменной this, что бы понять, что переменная _this заполнена правильным значением:
if (_this !== this) return methodCaller.call(this, arguments);
Соберём всё вместе и протестируем:
/** @author Vyacheslav Lapin<se-la-vy.blogspot.com>
 * @version 0.01 08.04.2009 17:52:40
 * @constructor
 * @param {number} x */
function A(x) {
    if (this.constructor !== A)
        return new A(x);
    this.privateState = this.createPrivateState(x);
}
A.prototype = new function() {
    var /** @type A */ _this,
        /** @static
         * @type Array<A> */
        __callInstances = [];
    this.createPrivateState = function(__x) {
        //Переменные для хранения private-полей объекта
        var /** @type number */ _x = __x,
            /** @constant
             * @type A */
            __this = this;
        this.createPrivateState = null;
        return {
            get: function() {
                if (_this) {
                    __callInstances.push(_this);
                    _this.privateState.set();
                }
                x = _x;
                //...
                _this = __this;
                _x =
                //...
                null;
            },
            set: function() {
                _x = x;
                //...
                x =
                //...
                _this = null;
                if (__callInstances.length
                    && __callInstances[__callInstances.length - 1] !== __this)
                    __callInstances.pop().privateState.get();
            }
        }
    }
    /** @param {Arguments} args */
    function methodCaller(args) {
        this.privateState.get();
        var result = args.callee.apply(_this, args);
        this.privateState.set();
        return result;
    }

    //--------------------------------------------------------------------------
    var /** @type number */ x;
    //...
    /** @return {number} */
    this.getX = function() {
        if (_this !== this) return methodCaller.call(this, arguments);
        return x;
    }
    /** @param {number} _x
     * @return {A} this */
    this.setX = function(_x) {
        if (_this !== this) return methodCaller.сall(this, arguments);
        x = _x;
        return this;
    }
    /** Метод, вызывающий себя же, но от другого объекта, переданного ему в
     * первом аргументе и передающий второй свой аргумент в качестве первого. В
     * случае, если первый аргумент не передан, выводится результат метода getX.
     * @param {A} [a1=null]
     * @param {A} [a2=null]
     * @param {A} [a3=null]
     * @return {number} */
    this.callMethod = function(a1, a2, a3) {
        if (_this !== this) return methodCaller.call(this, arguments);
        if (a1)
            return this.getX() + a1.callMethod(a2, a3);
        else
            return this.getX();
    }
}
A._className = 'A';
A.prototype.constructor = A;
//test
var /** @type A */ a1 = new A(1),
    /** @type A */ a2 = new A(2),
    /** @type A */ a3 = A(3),
    /** @type A */ a4 = A(4); //Можно вызывать и так - мы это предусмотрели в конструкторе
alert(a1.callMethod(a2, a3, a4));
Результат - "10" во всех браузерах, в которых я тестировал(IE, Opera, FF, Safari последних на данный момент стабильных версий). It works! :))))) На этом фрормирование первой версии "базовой модели" я считаю завершённым. Дальше в статьях данного цикла мы поговорим о том, как можно упростить разработку таких конструкторов с использованием инструментального средства Eclipse, а так же о том, как расширить "базовую модель", что бы применять её для наследования классов в JavaScript с сохранением преимуществ, которые даёт инкапсуляция на основе замыканий. А после этого мы поговорим о том, как расширить язык JavaScript, создав его диалект, направленный на более удобное использование базовой модели. Другие части: Часть 1: Основа Базовой модели Часть 2:Механизм вызова методов Часть 4: Наследование