вторник, 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;
    };
})();

2 комментария:

Dmitry комментирует...

А можно обойтись и без паразитных глобальных переменных:
function CEventController() {
var instance;
var actionCounter;

if (!instance)
instance = this;

this.stopActions = function() {
this.actionCounter++;
}
this.continueActions = function() {
this.actionCounter--;
}
this.isStopped = function() {
return this.actionCounter!=0 ? true : false;
}
this.getCounter = function() {
return this.actionCounter;
}

return instance;
}

И вызывать соответственно:
CEventController().stopActions();
alert(CEventController().getCounter());

Dmitry комментирует...

Неправильно я написал. Там создаются куча клонов объекта. В идеале: http://bitari.blogspot.com/2006/07/js-singleton_22.html