воскресенье, 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". Функция переопределена.

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

JavaScript: используйте arguments.callee вместо названия функции

Часто разработчики, когда им нужно изнутри функции сослаться на саму эту функцию, используют её имя, заявленное при её объявлении. Например, так:
var factorial = function(x){
    return x == 1 ? 1 : x + factorial(--x);
}
В данном коде мы, фактически, обращаемся к внешней цепочке областей видимости и читаем из неё переменную "factorial" и, надеясь, что это - функция, вызываем её.

Проблема заключается в том, что JavaScript - язык, в высшей степени чреватый конфликтами имён в случае, если над разработкой одного и того же функционала работает не один, а несколько человек. И в этой ситуации нельзя быть до конца уверенным, что переменной "factorial" в какой-то момент кто-нибудь не решит присвоить другую функцию или даже какое-нибудь значение типа числа, при этом функция может быть, скажем, записана в другую переменную и по этой причине разработчик, использующий эту функцию, может думать, что всё впорядке и функция должна работать:
var fac = factorial;
factorial = 27;
alert(fac(factorial));

Но функция выдаст ошибку:
factorial is not a function
factorial(26)test.js (line 2)
Просто нужно помнить, что функции в JavaScript - это такой особый тип данных, а не неизменяемая часть программы, как, например, в Java. Функцию можно присваивать любой переменной, свойству объекта или элементу массива, жонглируя ей как угодно. Эта возможность фактически означает, что у функций в JavaScript нет своих постоянных имён! Поэтому для того, что бы не лишать программистов этой возможности, лучше всегда изнутри функции ссылаться на неё с помощью встроенного и поддерживаемого свойства псевдо-массива Arguments, называемого "callee". Это свойство всегда ссылается на функцию, которая выполняется в данный момент, поэтому вы оказываетесь застрахованы от недоразумений, продемонстрированных выше:
function factorial(x) {
    return x == 1 ? 1 : x + arguments.callee(--x);
}
var fac = factorial;
factorial = 27;
alert(fac(factorial)); // Возвращает "378".
Но в JavaScript всё-таки есть способ элегантно обойти данную ситуацию. Если вам не очень нравится прибегать к вызову свойства callee объекта Arguments, то можно воспользоваться конструкцией, называемой "литералом функции" с необязательным параметром, содержащим имя функции:
var factorial = function f(x) {
    return x == 1 ? 1 : x + f(--x);
};
alert(f); // Ошибка: "f is not defined"
В этом коде не создаётся внешней переменной "f", здесь функция, как и в предыдущем примере, присваивается только внешней переменной "factorial", а переменная "f" определяется только внутри области видимости функции и служит для ссылки на данную функцию, так же, как и "arguments.callee", так что можно без опаски использовать переменную "f" для своих нужд - на работу данной функции это никак не повлияет.

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

среда, 14 мая 2008 г.

Программная обработка исключительных ситуаций в JavaScript

Зачем JS-приграммисту может понадобиться обрабатывать ошибки в сценариях? В принципе, не существует задач, при решении которых без механизма исключений никак нельзя было бы обойтись. Однако часто их использование оказывается заметно удобнее бесконечных if`ов - поскольку в коде мы с их помощью изолируем себя от некоторых ситуаций, в которых программа должна вести себя по-другому (главным образом, это может быть связано с неадекватной работой пользователя, например, при вводе данных) - и сосредотачиваемся на описании алгоритма в случае нужных нам условий, а уже потом отдельно разбираться, как говорится, с косяками. Это как в жизни - мы часто отодвигаем проблемы на потом, просто помним, что они у нас есть, и когда будет время - мы их решим, в противовес if`овому подходу, когда мы всё время должны решать проблемы реакции на неадекватные зигзаги сценария не отходя от кассы. По себе могу сказать, что писать сложный код так намного удобней - и более того, по мере усложнения кода использование исключений становится всё более и более необходимым. А если забыть обработать какое-то исключение, заявленное при написании "чистого" кода - браузер об этом скорее всего напомнит при тестировании.

Для тех, кто не знаком с механизмом исключений в Java


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

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

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


Для тех, кто знаком с механизмом исключений в Java


Здесь я кратко приведу различия между реализацией исключений в Java и JavaScript - пропустите этот раздел если с Java вы не знакомы.

Главное отличие от Java-реализации состоит в отсутствии (в силу не классовой, а прототипной реализации ООП) возможности использовать множественный оператор catch, основанный на наследовании и восходящем преобразовании. Здесь оператор catch может быть только один для каждого try`я. Этот приём можно менее изящно реализовать в обработчике на основе оператора switch.

Блок finally присутствует, но здесь он нужен ещё реже. Сам я, работая с JavaScript, ни разу не сталкивался с ситуацией, где бы он мне понадобился или был удобен - поэтому здесь я о нём тихонько умолчу, только намекнув, что он, в принципе, здесь есть...

Ну и, конечно, нет великолепной возможности (отсутствующей даже в C#) Java, обязывающей указывать возможность вызова исключения функцией при её описании с помощью ключевого слова throws, отсутствие какового сулит обернуться большой путаницей при использовании больших сторонних библиотек...

Объект исключения


Обычно в качестве объекта исключения выступает объект Error, хотя, строго говоря, конструкция throw может передавать абсолютно любой объект (!), который может быть принят оператором catch и соответственно обработан.

К сожалению, и область обработки исключительных ситуаций задела браузерная война - боевые действия в этой области привели к тому, что объекты Error в ECMAScript и в JScript практически не имеют ничего общего.

Какую информацию о состоянии может в себе нести исключение? По большому счёту всего 2 поля - некое название, характеризующие тип исключения, и сообщение, уточняющее особенности конкретного исключения в данном случае - если пользователь возбуждает исключение сам, то он придумывает сообщение сам, если его генерирует система - то уже она решает, какое сообщение писать. Прибавим к этим 2-м свойствам 3-е - prototype, для возможности изменять его характеристики ОО-средствами и 4-е стандартное toString() - и получим объект исключения от ECMAScript, в котором название называется "name" (в случае пользовательских объектов оно содержит строку "Error", а в случае ошибки браузера - название из небольшого списка, представленного в следующей таблице), а строка сообщения - свойство "message". Конструктор исключения от ECMAScript выглядит так:
new Error(message);
У Microsoft вместо имени у ошибки появляется составной номер (свойство "number"), состоящий из кода источника ошибки (facility code) и номера самой ошибки. Что бы выделить номер источника, можно воспользоваться выражением
(e.number >> 16) & 0x1FFF
, а что бы выделить код самой ошибки - выражением
e.number & 0xFFFF
, где e - объект Error), и "description", полностью аналогичное свойству "message" в ECMAScript (начиная с IE 5.5 значение свойства "description" доступно и через свойство "message", т.е. по существу, они - синонимы, ссылающиеся на одно и то же значение). Соответствие номеров описаниям для JScript можно найти в этой таблице.
Конструктор объекта исключения от Microsoft IE выглядит так:
new Error(errorNumber, message);
- этот формат не совместим с приведённым ранее форматом, соответственно, к сожалению, для различных браузеров придётся писать разные возбуждения исключений...

Впрочем, популярный особенно в Linux-среде open-source`ный браузер Mozilla так же нередко расширяет стандарты (хотя существующие - поддерживает, в отличае от IE). В объекте Error для этого браузера добавлено 2 интересных свойства для локализации исключения - "fileName" и "lineNumber", хранящие, соответственно, информацию о файле, где произошла ошибка (актуально, когда в проекте много *.js - файлов и непонятно в котором из них ошибка) и о номере строки в этом файле.

Пример использования


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

Отметим, что оператор1 может включать вызовы функций - если в них возникнут исключения и там не окажется более глубоко вложенных блоков try..catch, внутри которых оно будет вызвано, то их поймает именно этот catch.

Теперь о самостоятельном вызове с помощью оператора throw. Как и в других темах, существует 2 способа писать универсальный код:
  • Различать браузеры и писать разный код для разных браузеров, и
  • Писать более простой код, который относится к области пересечения браузеров.
Я нахожу более мудрым второй способ и по возможности стараюсь использовать его. В данном случае это означает, что лучше отказаться от объекта Error из-за разного синтаксиса конструкторов и присваивать исключению простую строку:
function getMonthName(month) {
    month--; // Переводим month в индекс массива (1=январь, 12=декабрь)
    var months=["январь","февраль","март","апрель","май","июнь","июль",
"август","сентябрь","октябрь","ноябрь","декабрь"];
    if (months[month] != null)
        return months[month];
    else
        throw "Неверный месяц";
}

try {
    monthName = getMonthName(myMonth); // возможно исключение
} catch (e) {
    monthName="неизвестно";
}
document.write(monthName);
Ссылки по теме для более глубокого изучения:
1) Ю.С. Лукач, "Справочник Web-разработчика", раздел "Обработка исключений";
2) Степанищев Евгений: Блоки try... catch... finally... в JScript 5. Статья на сайте CitForum.ru

P.S. Это одна из ранних моих статей, опубликованных на форуме программистов "Vingrad". Ознакомиться с комментариями Vingrad`цев можно тут.