вторник, 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: Наследование

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

В предыдущей статье была обозначена проблема поиска совместимости инкапсуляции на основе замыканий с принципом делегирования методов объектов, созданных одним конструктором, объекту-прототипу. В качестве возможного решения была продемонстрирована модель создания специального генератора объектов (называемых "privateState") внутри прототипа, в котором находятся методы и набор закрытых полей. Каждый из порождаемых генератором объектов в этой модели содержит вложенный набор переменных (переменных объекта), соответствующих переменным прототипа, а так же двух методов:
  • "get" - присваивание значений переменных объекта - переменным прототипа,
  • "set" - присваивание значений переменных прототипа - переменным объекта.
У этой модели имеется ряд неудобств при оформлении кода и ограничений при работе с получающимися объектами, как то:
  • Необходимость в начале каждого метода вставлять вызов метода "get" и в конце - метода "set";
  • Неправильное поведение при вызове одним методом объекта других методов того же объекта;
  • Необходимость каждый раз вписывать имена переменных в методы "set" и "get";
  • Неправильное поведение при вызове одним объектом другого объекта, являющегося потомком того же класса.
Попыткам разрешить эти неудобства и снять ограничения посвящена данная и последующие статьи данного цикла. Тема данной статьи - разрешение первых двух из приведённых пунктов. Мне хотелось бы разработать максимально удобный паттерн использования инкапсуляции с ООП на основе прототипов, поэтому необходимость при написании каждого метода писать 2 строки кода - одну ("this.privateState.get();") в начале метода, вторую ("this.privateState.set();") - в конце, не может не резать мне глаз. Итак, что мы могли бы сделать, что бы сократить эти две строчки? Было бы здорово, если бы у нас была возможность поставить некоторый фильтр на вызовах всех методов прототипа, производимых через объект. Мы могли бы просто вписать перед ним вызов get, а после вызова самого метода - вызов set и - дело с концом. Но такой возможности у нас нет, поэтому нам придётся этот фильтр всё-таки вызывать из каждого метода, т.е. от одной строки - вызова. Назвать данный метод можно "methodCaller", т.к. он будет ответственным за вызовы методов. Нетрудно будет его написать:
/** @returns {Object} результат выполнения метода в контексте полей
 * данного объекта. */
function methodCaller() {
    this.privateState.get();
    var /** @type Object */ result = //вызов вызвавшей данную функции
    this.privateState.set();
    return result;
}
Как можно было бы передать этому методу имя и параметры вызова той функции, которая его вызвала, что бы он узнал, какой именно метод и с какими параметрами ему нужно будет вызвать? Можно было бы передать функцию - первым аргументом, а параметры - массивом во втором аргументе, но есть вариант по-проще - мы можем передавать ссылку "arguments". Она содержит и параметры вызова нашей функции, являясь их псевдо-массивом и ссылку на неё саму (она содержится в поле "callee"). Необходимо помнить, что при вызове функции как метода объекта нам придётся пользоваться одним из методов объекта Function - "call" или "apply", которые предоставляют контроль над ссылкой "this" внутри неё, что бы подставлять методу правильную ссылку "this", иначе она будет вести не туда, куда нужно, что может привести к неадекватной работе метода. Кроме того, следует учесть, что метод может вызывать те или иные исключения. Несмотря на это, для него в любом случае необходимо вызвать метод "set". Для того, что бы этого добиться, поместим вызов этого метода в блок finally, а вызов исконного - в блок try перед ним. Тогда мы сможем элегантно избавиться от создания специальной переменной "result", поскольку блок "finally" выполняется даже после выполнения команды return. Итак, результирующий метод:
/** @returns {Object} результат выполнения метода в контексте полей
 * данного объекта. */
function methodCaller(args) {
    this.privateState.get();
    try {
        return args.callee.apply(this, args);
    } finally {
        this.privateState.set();
    }
}
Теперь рассмотрим возможный механизм вызова данного метода:
this.getX = function() {
    if (/* если вызов происходит извне */)
        return methodCaller.call(this, arguments);
    return x;
}
Осталось только придумать, как же все наши методы могли бы узнать, извне они вызваны или изнутри - методом "methodCaller"? В принципе, у объекта Function есть ссылка на функцию, вызвавшую данную - эта ссылка находится в свойстве caller и доступна только внутри функции. Мы могли бы написать выражение так:
if (arguments.callee.caller !== methodCaller) // если вызов происходит извне
, однако, к сожалению, метод caller не входит в стандарт ECMAScript и поэтому считается устаревшим. Почти все реализации JavaScript его до сих пор поддерживают, но у нас нет никаких гарантий того, что он будет поддерживаться дальше. Так что нужен какой-то другой механизм. Вспомним, что в первой статье данного цикла мы решили об-null-ять ссылки прототипа при окончании выполнения им методов, что бы освобождать память. Таким образом, на момент вызова метода извне, когда метод privateState.get ещё не вызван, у нас все закрытые поля ссылаются на null. Т.е. мы могли бы узнать, произведён вызов метода снаружи или изнутри методом methodCaller, просто проверив любое из полей объекта на равенство null, но вот незадача - мы не можем точно знать, что поле объекта, которое выберем для этого, не окажется равным null по логике работы самого класса и, соответственно, что мы не ошибёмся в нашем выводе о том, кто вызвал метод и не вызовем бесконечный цикл, в котором метод будет вызывать methodCaller, а тот в свою очередь - опять тот же метод. Иными словами, нам нужна такая ссылка в каждом объекте, про которую у нас всегда будет известно, что она - не null. Можно было бы сделать какой-нибудь простой служебный флаг типа boolean, который был бы скрытым полем любого объекта и всегда был бы равен true, что бы по его состоянию каждый метод мог определённо сказать - применён метод privateState.get или не применён, но есть идея по-лучше: помните, я в начале статьи писал о необходимости всегда помнить о том правиле, что приватные методы нам придётся вызывать не как обычно, просто открывая за их именами круглые скобки и перечисляя параметры, а с использованием специальных методов объекта function - call или apply, поскольку нам нужно явно указывать JavaScript-интерпретатору, что в этих методах ссылка this должна вести на объект, иначе она будет вести не туда. Помните? Так вот, теперь у нас появляется возможность обойти это жёсткое правило, приняв определённое соглашение. Т.к. нам всё равно нужна некоторая флаговая переменная для того, что бы мы по ней смогли гарантированно отличить, выполнен уже для объекта метод privateStage.get() или нет, мы можем этой переменной присвоить ссылку на сам этот объект. :) Т.е. введём специальную переменную в прототипе, назовём её _this, и присвоим ей ссылку this. Можно будет из любого закрытого метода использовать её вместо обычной ссылки this и тогда станет возможно вызывать эти методы обычным образом - как функциии, без использования таких дополнительных методов объекта function, как call и apply. ;) Тогда в начале private`ных методов мы можем писать следующую строчку:
if (_this !== this) return arguments.callee.apply(_this, arguments);
и это гарантирует нам правильную работу ссылки this в них. Т.е. теперь механизм вызова методов будет выглядеть так:
function methodCaller(args) {
    this.privateState.get();
    try {
        // Обратите внимание - теперь мы можем использовать ссылку "_this",
        // которая доступна между вызовами методов объекта privateState -
        // "get()" и "set()".
        return args.callee.apply(_this, args);
    } finally {
        this.privateState.set();
    }
}
Теперь рассмотрим возможный механизм вызова данного метода:
this.getX = function() {
    if (!_this) // если вызов происходит извне
        return methodCaller.call(this, arguments);
    return x;
}
И теперь обратим внимание, что мы решили и вторую проблему из списка проблем вначале статьи - "Неправильное поведение при вызове одним методом объекта других методов того же объекта", состоявшее в том, что метод "get" объекта privateState вызывается несколько раз, а потом происходят серии вызовов методов "set". Теперь, если наш метод "getX" вызовет какой-либо другой метод своего объекта, то этот метод пройдёт проверку в начале, т.к. у него уже установлено значение переменной "_this", ведь метод "privateState.set()" у него ещё не вызывался - он вызовется только когда метод "getX" завершит работу. Подведём предварительный итог, приведя весь получившийся код:
/** @constructor
 * @author Vyacheslav Lapin<se-la-vy.blogspot.com> */
function A(x) {
    this.privateState = this.createPrivateState();
    this.setX(x);
}
A.prototype = new function() {
    var /** @type A */ _this,
        /** @type number */ x;
    /** Конструктор объекта состояния private-полей объекта. Метод предназначен
     * для вызова один раз и только из конструктора объекта. Поэтому после
     * вызова он перекрывает объекту доступ к себе, посредством установки у
     * объекта одноимённого свойства, равного null
     * @constructor
     * @return {Object} - privateState-объект для конструктора A */
    this.createPrivateState = function() {
        //Переменные для хранения private-полей объекта
        var /** @type number */ _x,
            /** @constant
             * @type A */
            __this = this;
        // Закрываем возможность вызова этого метода объектом в дальнейшем
        this.createPrivateState = null;
        return {
            get: function() {
                //Присвоение переменным прототипа значений private-полей объекта
                x = _x;
                _this = __this;
                // Об-null-ение переменной объекта на время, пока её значение
                // находится в прототипе, воизбежание возможных проблем со
                // сборкой мусора
                _x = null;
                //, а ссылку "__this" нет смысла об-null-ять - сборке мусора
                // никак не поможет, если мы на время выполнения метода запретим
                // объекту privateState ссылаться на объект, для которого он
                // создан.
            },
            set: function() {
                // Возвращение значения private-полю объекта после использования
                _x = x;
                // Возвращать значение ссылки "__this" из прототипа так же не
                // имеет смысла - по сути она - константа и с ней не должно
                // ничего происходить.
                //Об-null-ение переменных прототипа
                x =
                _this = null;
            }
        }
    }
    /** Cлужебный метод для вызова методов объекта,
     * которые используют его private-переменные. В методах, которые не
     * используют private-поля его вызывать не обязательно.
     * @param {Arguments} args */
    function methodCaller(args) {
        this.privateState.get();
        try {
            return args.callee.apply(_this, args);
        } finally {
            this.privateState.set();
        }
    }
    /** @return {number} x */
    this.getX = function() {
        if (!_this) return methodCaller.call(this, arguments);
        return x;
    }
    /** @param {number} newX Значение X для установки */
    this.setX = function(newX) {
        if (!_this) return methodCaller.call(this, arguments);
        if (typeof newX == 'number')
            x = newX;
        else
            if (typeof newX == 'object'
                && newX instanceof Number)
                x = newX.valueOf();
            else
                throw new Error(
                    'Param is not a number! Param type is ' + typeof newX
                );
    }
    /** Функция для проверки корректности вызовова методов внутри объекта */
    this.getX2 = function() {
        if (!_this) return methodCaller.call(this, arguments);
        return this.getX() * this.getX();
    }
}
if (!A.hasOwnProperty('name'))
    /** @constant
     * @type string
     * @ignore */
    A.name = 'A';
A.prototype.constructor = A;

// Тестируем:
var a1 = new A(5);
alert('a1.getX() = ' + a1.getX()); // Выводит 'a1.getX() = 5'
alert('a1.getX2() = ' + a1.getX2()); // Выводит 'a1.getX2() = 25'

alert(a1.createPrivateState); // Выводит 'null' - метод не доступен

var a2 = new A(40),
    a3 = new A(new Number(562)),
    a4 = new A(0);
alert('a2.getX() = ' + a2.getX()); // Выводит 'a2.getX() = 40'
alert('a2.getX2() = ' + a2.getX2()); // Выводит 'a2.getX2() = 1600'

alert('a3.getX() = ' + a3.getX()); // Выводит 'a3.getX() = 562'
alert('a3.getX2() = ' + a3.getX2()); // Выводит 'a3.getX2() = 315844'

alert('a4.getX() = ' + a4.getX()); // Выводит 'a4.getX() = 0'
alert('a4.getX2() = ' + a4.getX2()); // Выводит 'a4.getX2() = 0'

alert('a1.getX() = ' + a1.getX()); // Выводит 'a1.getX() = 5'
alert('a1.getX2() = ' + a1.getX2()); // Выводит 'a1.getX2() = 25'
Другие части: Часть 1: Основа Базовой модели Часть 3: Внутренние вызовы Часть 4: Наследование

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

Когда-то Дуглас Крокфорд впервые заметил, что на основе замыканий (closures) можно имитировать инкапсуляцию примерно так:
function A() {
    var /* number */ x = 5;
    this.getX = function() { return x;}
    this.setX = function(newX) {
        if (typeof x == 'number')
            x = newX;
        else
            throw new Error('Param is not a number!');
    }
}

var y = new A();
alert(y.getX()); // Вернёт 5
alert(y.x); // Вернёт 'undefined'
Вроде бы, выглядит логично, не правда ли? Вот только есть одна проблема, на которую в своё время обратил моё внимание участник форума Vingrad с ником AKS: каждый объект содержит весь набор методов своего класса, и если объектов будет достаточно много, то этот метод приведёт к очень неэкономному расходованию памяти.

JavaScript относится к языкам с ООП, основанным на прототипах (prototype-based OOP). В данном случае это значит, что у объектов с большим количеством методов, что бы сэкономить память, рекомендуется эти методы выносить в общий для них прототип. Это легко сделать, отказавшись от инкапсуляции и сделав эти поля открытыми, что бы методы прототипа каждый раз могли их считывать в зависимости от того, у какого объекта они вызваны. Но как добиться этого, не отказываясь от инкапсуляции для этих полей?

Т.е. существует противоречие между инкапсуляцией на основе замыканий и принципами ООП на базе прототипов.

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


Итак, наиболее удобным контейнером для методов класса является, конечно, его прототип (в нашем случае - A.prototype). На него ссылаются все объекты данного класса и, если вызываемый метод не находится в них, то следует вызов метода из их прототипа. Так что задача состоит в том, что бы позволить методам прототипа работать с закрытыми полями объектов, из которых эти методы вызываются.

Объект-прототип является чужим для того замыкания, в котором находятся поля. Он связан с ним неявной ссылкой (в FF она носит название "__proto__"), но она не поможет ему считать закрытые переменные. Так что же делать?

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

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

Тогда какими должны быть требования к этому вспомогательному замыканию? Оно должно уметь делать две вещи:
  • Устанавливать в закрытые переменные прототипа значения закрытых переменных объекта;
  • Записывать в закрытые переменные объекта значения закрытых переменных прототипа (после того, как прототип произвёл с ними некоторые действия).
Т.к. одного действия нам не достаточно, это не может быть функция - это должен быть объект с двумя методами:
  • метод "get" выполняет взятие значений переменных из объекта, при этом самим переменным объекта неплохо было бы присвоить значение null, что бы не иметь дублирующихся ссылок;
  • метод "set" устанавливает значения обратно в объект, об-null-яя аналогичные переменные в прототипе.
Сам этот объект назовём "privateState", поскольку он несёт закрытую часть состояния объекта.

Итак, что нам в итоге нужно:
  • метод для генерации внутреннего замыкания в прототипе объекта, который будет возвращать объект с двумя методами: get и set, которые, соответственно, будут устанавливать в прототип значения закрытых полей из этого объекта и в объект - из прототипа;
  • механизм вызова для методов прототипа, гарантирующий, что перед выполнением будут установлены значения закрытых полей.
Можно было бы, конечно, просто сделать внутри прототипа массив, который содержал бы privateState-объекты, а каждый объект просто содержал бы некоторый идентификатор, по которому прототип узнавал бы, с каким множеством значений ему нужно в данный момент работать. Но в таком случае мы не сможем рассчитывать на сборку мусора этих полей - т.к. ссылка содержится в прототипе, а в объекте - лишь идентификатор, то при уничтожении объекта у нас не будет возможности узнать о том, что следует удалить и принадлежащее ему множество закрытых полей (его privateState-объект) - и они останутся в памяти при том, что использоваться не будут. В итоге мы получим проблему с утечкой памяти. Поэтому единственная ссылка на privateState-объект должна быть у объекта, которому она принадлежит, прототип ссылаться на неё не должен.

Итак, метод генерации privateState-объекта внутри прототипа может выглядеть примерно так:
// Переменная прототипа, в которую будем помещать значение private-поля
// объекта каждый раз, когда будем выполнять какой-либо его метод.
var x;

// Функция, являющаяся конструктором provateState-объекта
this.createPrivateState = function() {

    //"_x" - переменная для хранения private-поля объекта
    var _x;

    return {
        get: function() {
            //Присвоение переменной прототипа значения private-поля объекта
            x = _x;

            // Об-null-ение переменной объекта на время, пока её значение
            // находится в прототипе, во избежание дублирующейся информации
            _x = null;
        },

        set: function() {
            // Возвращение значения private-полю объекта после использования
            _x = x;

            //Об-null-ение переменной прототипа
            x = null;
        }
    }
}
При этом нам нужно так же продумать механизм вызова методов. В наиболее простейшем варианте мы могли бы сделать это так:
this.method1 = function() {
    this.privateState.get();

    //здесь располагается код метода, работающего с private-полями...

    this.privateState.set();
}
Тогда в конструкторе объекта мы могли бы просто вызывать метод createPrivateState() прототипа:
function A() {
    this.privateState = this.createPrivateState();
}

A.prototype = new function() {
    var x;
    this.createPrivateState = function(){...} //Описан выше
    this.method1 = function() {...} //Описан выше
    //Другие методы...
}

A._className = "A"; //Записываем в специальное свойство класса его имя
A.prototype.constructor = A; //Присваиваем прототипу ссылку на конструктор
Однако для промышленного применения этот метод не годится по ряду причин. Во-первых, мы не учли ситуации, когда один метод вызывает другой метод того же объекта (что делается довольно часто), во-вторых мы не учли, что в методе может происходить вызов метода другого объекта этого же класса. И, наконец, механизм вызова, состоящий из двух строчек вначале и в конце объявления метода выглядит излишне-громоздким. Тому, как наиболее элегантно справиться с этими и более мелкими проблемами, будут посвящены следующие статьи цикла "Особенности инкапсуляции на основе замыканий". :)

Другие части:
Часть 2: Механизм вызова методов.
Часть 3: Внутренние вызовы.
Часть 4: Наследование.

JavaScript: Дополняем объект Date

На мой взгляд, объекту Date в JavaScript не хватает небольшого количества констант для более удобной работы с ним. Вот как можно все их создать для него:
// Adding consts for comfortable work with Date constructor.
/** @type {number}*/ Date.MS_PER_YEAR = 365 * (
    /** @type {number} */ Date.MS_PER_DAY = 24 * (
        /** @type {number} */ Date.MS_PER_HOUR = 60 * (
            /** @type {number} */ Date.MS_PER_MINUTE = 60 * (
                /** @type {number} */ Date.MS_PER_SECOND = 1000
            )
        )
    )
);

/** @type {number} */ Date.MS_PER_LEAP_YEAR =
                            Date.MS_PER_YEAR + Date.MS_PER_DAY;
LEAP YEAR - это високосный год.
Т.к. с датой чаще всего бывает удобнее всего работать как с числом, возвращаемым методом getTime() и передавать число в конструкторе, то эти константы часто помогают производить более сложные операции.

Update №1 (18.01.2010):
Учитывая, что дату и время в объекте Date можно менять, периодически возникает задача вернуть объект Date, на который ориентируется внутренняя структура какого-либо объекта и который в нём инкапсулирован (при помощи замыканий) и изменения в котором могут негативно отразиться на логике его работы. Для таких случаев в Java предназначен метод clone. Он возвращает точную копию объекта. Вот какой могла бы быть его реализация:
Date.prototype.clone = function() {
    return new Date(this.getTime());
};

JavaScript: небольшое отличие function от function

Некоторое время назад я писал на форуме, что не вижу разницы между следующими конструкциями:
function fnName() {
//...
}
, и
var fnName = function () {...}
, но на самом деле разница есть. Состоит она в том, что в первом случае функция определяется на этапе синтаксического анализа, а во втором - в момент выполнения. Что бы её продемонстрировать, давайте посмотрим на следующий пример:
x(); // Выводит "1", хотя функция ещё вроде бы не определена...
var x = 2;
alert(typeof x); // Выводит "number"
// Следующая строка выполнилась ещё до того, как выполнилась первая строка данного
// примера, так что сейчас она ничего не меняет.
function x(){alert(1);}
alert(typeof x); // Опять выводит "number"
var x = function(){alert(3);}
x(); // Выводит "3". Функция переопределена.