JS中的单例模式和抽象

时间:2009-11-23 14:08:06

标签: javascript design-patterns extjs oop

虽然下面的示例利用ExtJS,但可以很容易地推断到另一个框架。我是抽象和数据隐藏的粉丝(和OO一般);是否还有其他人隐藏数据和成员/功能,或者您认为这种尝试是否过度杀伤?

(注意:我强烈认为DOM ID几乎不应该被硬编码。而且,虽然我将原型用于典型类的公共方法,但您会注意到在原型之外创建的公共函数。)

这个说明很有意思http://yuiblog.com/blog/2007/06/12/module-pattern/

Ext.ns('Foo.Bar');

/**
 * Foo.Bar.MainToolbar (singleton)
 */
Foo.Bar.MainToolbar = (function()
{
  // Temporary, private class used to create and return an object - a singleton
  var toolbarClass = Ext.extend( Ext.Container,
  {
    /**
     * constructor (public)
     */
    constructor: function( config )
    {
      config = config || {};

      // PRIVATE MEMBER DATA ========================================

      // Need IDs for animation anchors
      var accountId = Ext.id( null, 'ls-accountDiv-');
      var faqId = Ext.id( null, 'ls-faqDiv-');
      var logoutId = Ext.id( null, 'ls-logoutDiv-');
      var loginId = Ext.id( null, 'ls-loginDiv-');
      var rulesId = Ext.id( null, 'ls-rulesDiv-');

      var itemCls =
        'color: white; cursor: pointer; font-weight: bold; ' +
        'font-family:Helvetica,Arial,Verdana,Lucida Sans Unicode,Sans-serif;';


      // PUBLIC METHODS *********************************************

      /**
       * userLogin (public) -
       */
      this.userLogin = function( userName, password )
      {
        // Update title bar
        Ext.fly(accountId).update( userName );
        Ext.fly(loginId).hide(true);
        Ext.fly(logoutId).show(true);
      };

      // PRIVATE METHODS ********************************************

      /**
       * handleFaqClick (private) - handler for click on FAQ
       */
      var handleFaqClick = function( event )
      {
        var dialogMsg = '<div style="text-align: leftblah, blah</div>';

        Ext.Msg.show({
          title: 'FAQ',
          modal: true,
          msg: dialogMsg,
          animEl: faqId,
          buttons: Ext.Msg.OK,
          icon: Ext.MessageBox.QUESTION,
          minWidth: '700'
        });
      };

      /**
       * handleLogoutClick (private) - handler for click on logout
       */
      var handleLogoutClick = function( event )
      {
        Ext.fly(accountId).update('');
        Ext.fly(logoutId).hide(true);
        Ext.fly(loginId).show(true);
      };

      /**
       * handleRulesClick (private) - handler for click on RULES
       */
      var handleRulesClick = function( event )
      {
        var dialogMsg = 
          '<div style="text-align: left;"><br/><b>blah, blah</div>';

        Ext.Msg.show({
          title: 'Rules',
          modal: true,
          msg: dialogMsg,
          animEl: rulesId,
          buttons: Ext.Msg.OK,
          icon: Ext.MessageBox.INFO,
          minWidth: '700'
        });
      };


      // CONSTRUCTOR  ===============================================

      // Some parameters (possibly) offered by the user are ignored
      config.id = Ext.id( null, 'ls-mainToolbar-');
      config.layout = 'absolute';
      config.layoutConfig = {};
      config.height = 38;
      config.width = 968;

      config.items = [
      {
        id: Ext.id( null, 'ls-mainToolbar-'),
        xtype: 'box', x: 25, y: 0, height: 36, 
        autoEl: { tag: 'img', src: './images/top_toolbar.png' }

      },{
        id: Ext.id( null, 'ls-logo-'),
        xtype: 'box',
        x: 70, y: 8, height: 22, width: 200,
        autoEl: { style: itemCls, html: 'Foo Bar' }
      },{
        id: accountId,
        xtype: 'box',
        x: 470, y: 8, height: 22, width: 200,
        autoEl: { style: itemCls + ' text-align: right;', html: ' ' }
      },{
        id: logoutId,
        xtype: 'box', x: 730, y: 8, height: 22, width: 36,
        autoEl: {style: itemCls + ' visibility: hidden;', html: 'logout'},
        listeners:
          { render:
            function( cmp ){
              cmp.getEl().addListener('click', 
                handleLogoutClick.createDelegate(this))
            }.createDelegate(this)
          }
      },{
        id: loginId,
        xtype: 'box', x: 730, y: 8, height: 22, width: 36,
        autoEl: { style: itemCls, html: 'login' },
        listeners:
          { render:
            function( cmp ){
              cmp.getEl().addListener('click',
                Foo.Bar.LoginDialog.show.createDelegate(
                  Foo.Bar.LoginDialog, [Ext.emptyFn]))
            }
          }
      },{
        id: rulesId,
        xtype: 'box', x: 800, y: 8, height: 22, width: 36,
        autoEl: { style: itemCls, html: 'rules'},
        listeners:
          { render:
            function( cmp ){
              cmp.getEl().addListener( 'click', 
                handleRulesClick.createDelegate(this) )
            }.createDelegate(this)
          }
      },{
        id: faqId,
        xtype: 'box', x: 860, y: 8, height: 22, width: 26,
        autoEl: { style: itemCls, html: 'faq'},
        listeners:
          { render:
            function( cmp ){
              cmp.getEl().addListener( 'click', 
                handleFaqClick.createDelegate(this) )
            }.createDelegate(this)
          }
      }];

      toolbarClass.superclass.constructor.apply( this, [config] );

      Foo.Bar.LoginDialog.addListener(
        Foo.Bar.LoginDialog.LOGIN_SUCCESSFUL_EVENT(), 
          this.userLogin.createDelegate(this));
    }
  });

  return new toolbarClass();
})();

3 个答案:

答案 0 :(得分:2)

谨慎地隐藏JavaScript中的数据通常是一种矫枉过正,但它也可能是一个非常好的主意,特别是当您创建一个库并希望消费者使用该库的公共API而不是弄乱内部时(很多富有想象力的人都喜欢这样。)

在JavaScript中,隐藏数据/方法的模式通常会创建一个闭包,其中包含所有私有内容,并让您的公共API方法可以访问该闭包。

简单示例:

var API = (function() {
    // internal stuff goes in here
    // ...
    // some public methods i'll expose later
    // all have access to the internals since they're inside the closure
    function method1() { ... }
    function method2() { ... }
    var somevar;
    return {
        public_method1: method1,
        public_method2: method2,
        public_var: somevar,
    };
})();

// use the API:
API.public_method1();

答案 1 :(得分:0)

我会发布一个我的大型项目的片段,希望它对你有用。我不是专业人士所以你可能会发现愚蠢或糟糕的事情。我正在使用的框架是Prototype。

缺少很多代码,我希望你能理解这个结构。

CORE.ELEMENT.BaseInterface是一个混合。

如果您有疑问,请告诉我。

function getRandomNumber() {
    return 4; // Chosen by fair dice roll. Guaranteed to be random. Thanks to xkcd.com for this function.
}

/*  -------------------------------
    Core
    -------------------------------
*/

var CORE = function () {
    var mix = function () {
        /* Merge the properties of all the objects given as arguments into the first object, making sure only the first object is modified. Of all the properties with the same name belonging to different objects, the one belonging to the rightmost object wins; that is, precedence goes right to left. */
        var args = $A(arguments);
        if (!args[0]) {
            args[0] = {}; // probably got an empty prototype or something.
        }
        for (var l = args.length, i = 1; i < l; ++i) {
            Object.extend(args[0], args[i]);
        }
        return args[0];
    }

    return {
        mix: mix
    }
}();


var INTERFACE = (function(){

    Notifier = function () {
        CORE.mix(this, {
            max: 5, // max number of notifications shown
            timeout: 8 // a notification disappears after this number of seconds
        }, arguments[0] || {});
        this.elm = ELM.DIV({ // ELM.DIV is too long to explain, it's some trickery I got partly from script.aculo.us - the idea at least.
            attributes:{
                id:'notifier',
                style: 'display: none;'
            }
        })
    };

    CORE.mix(Notifier.prototype, CORE.ELEMENT.BaseInterface, {
        notify: function (msg) {
            if (this.elm) {
                var notes = this.elm.getElementsBySelector('.notification');
                while (notes.length >= this.max) {
                    notes.last().remove();
                }
                this.elm.insert({top: '<div class="notification" style="display: none;">' + msg + '</div>'});
                if (!this.elm.visible()) {
                    this.elm.setStyle('opacity: 0; display: block;');
                    this.elm.morph('opacity: 1;', {
                        duration: 1
                    });
                }
                var newNote = this.elm.down('div');
                newNote.setStyle('opacity: 0; display: block;');
                newNote.morph('opacity: 1;', {duration: 1});
                this.removeNotification.bind(this).delay(this.timeout, newNote);
            }
        },
        removeNotification: function (note) {
            note.remove();
            var notes = this.elm.getElementsBySelector('.notification');
            if (notes.length === 0) {
                this.elm.hide();
            }
        }
    });

    return {
        Notifier: new Notifier() //singleton
    };

})();

/*global Ajax, INTERFACE, CONFIG, CORE, Template $ */

var CONTENT = function () {

    var wallpapers = [];

    wallpapers.toJSON = function () { // needed if I want to send a list of wallpapers back to the server
        var tmp = [];
        this.each(function (wp) {
            if (wp.elm.visible()) {
                tmp.push(wp.toJSON());
            }
        });
        return '[' + tmp.join(', ') + ']';
    };

    var newWallpapers = []; // just a buffer

    Wallpaper = function () {
        CORE.mix(this, {
            thumbUrl: '',
            view: '',
            width: 0,
            height: 0,
            source: ''
        }, arguments[0] || {});
        this.aspect = this.width / this.height;
        switch (this.aspect) {
        case 1.6:
            this.aspect = 2;
            break;
        case 16 / 9:
            this.aspect = 2;
            break;
        case 5 / 4:
            this.aspect = 1;
            break;
        case 4 / 3:
            this.aspect = 1;
            break;
        default:
            if (this.width > 2500) {
                this.aspect = 3;
            } else {
                this.aspect = 0;
            }
        }
        this.dimension = this.width < 1280 ? 0 : (this.width < 1680 ? 1 : (this.width < 1920 ? 2 : 3 ));
        this.hr_aspect = CONFIG.labels.aspect[this.aspect];
        this.hr_source = CONFIG.labels.source[this.source].capitalize();
        this.html = '<div class="source">' + this.hr_source + '</div><a class="thumb" target="_BLANK" href="'+ this.view + '"><img class="thumb" src="' + this.thumbUrl + '" /></a><div class="info"><div class="resolution">' + this.width + 'x' + this.height + '</div><div class="aspect">' + this.hr_aspect + '</div></div>';
    };

    CORE.mix(Wallpaper.prototype, CORE.ELEMENT.BaseInterface, {
        fxParms: null,
        getElement: function () {
            this.elm = document.createElement('div');
            this.elm.className="wallpaper";
            this.elm.innerHTML = this.html;
            return this.elm;
        },
        postInsert: function () {
            if (this.thumbHeight) {
                var x = this.thumbHeight * 200 / this.thumbWidth;
                this.elm.down('img.thumb').setStyle('margin: ' + ((200 - x) / 2) + 'px 0 0;');
            }
            delete this.html;
        },
        toJSON: function () {
            return Object.toJSON({
                thumbUrl: this.thumbUrl,
                view: this.view,
                width: this.width,
                height: this.height,
                source: this.hr_source,
                aspect: this.hr_aspect
            });
        }
    });

    return {
        wallpapers: wallpapers, // wallpapers being shown
        newWallpapers: newWallpapers, // incoming wallpapers
        Wallpaper: Wallpaper // constructor
    };

}();

这就是我现在制作名称空间的方式。如果我在最后一次“返回”语句中没有返回任何内容,它会因为关闭而幸存,或者被垃圾收集器吃掉。如果你不习惯它,看起来像一团糟,我想。好吧,如果您发现任何值得询问的内容,请与我联系。


以防它不明显(可能不是);在命名空间底部的return语句中,它的工作方式如下:

  • 如果它是大写的(例如壁纸:壁纸),总是一个构造函数
  • 如果是“新......”声明,总是单身
  • 如果它没有大写(例如newWallpapers:newWallpapers),那么 一个简单的函数,意味着在没有“新”简单对象的情况下调用

答案 2 :(得分:0)

如果您正在为自己的内部使用编写应用程序代码,那么在JS中隐藏成员并不是那么有用。唯一的目的是阻止某人访问您隐藏的任何内容,并且只有当您专门编写供他人使用的代码并且您希望(强烈)强制执行A​​PI时,才能提供好处。请注意,即使在Ext JS的情况下,如果查看大多数类,API通常不是通过约定而不是通过闭包来实现(私有事物只是标记为私有),因此您可以覆盖和扩展需要的东西。当它真的不应该被搞砸时,有私人的东西,但这是例外。这就是它如此强大的框架 - 几乎所有东西都是可扩展的。所以这真的取决于你希望你的最终代码有多严格。