`
houfeng0923
  • 浏览: 142731 次
  • 性别: Icon_minigender_1
  • 来自: 大连
社区版块
存档分类
最新评论

JavaScript设计模式摘要(一)

阅读更多

 

-----------------------------------------第一部分 面向对象的JavaScript---------------------------------------------- 

 

-----------------------------------------第一章:富有表现力的JavaScript----------------------------------------------- 

 

JavaScript强大的表现力赋予程序员在运用设计模式编写代码时极大的创造性。在JavaScript中使用模式主要有如下原因:

1,可维护性    降低模块耦合,方便重构和更新模块

2,沟通        模式为不同类型对象提供了通用的术语,使交流变得容易。

3,性能        某些模式起到性能优化的作用。如提高程序运行速度,减少代码量和传输数据量。

而不适用模式的原因如下:

1,复杂        获得可维护的代价使程序变得复杂,不易被新手理解。

2,性能        多数模式对代码的性能有所影响,具体影响取决于项目的具体需求。

 

因此,JavaScript中使用设计模式不要产生负面效果,过度复杂的架构会很快把应用程序拖入泥沼。你使用的编程风格和选择的设计模式应该与所要完成的具体工作相称。

 

--------------------------------------------第二章:接口------------------------------------------------------------------

 

JavaScript没有内置的创建或实现接口的方法。好在JavaScript的灵活性,通过考察其他面向对象语言实现接口的方法,并对它们在这方面最突出的特性进行模仿

设计一个可重用的类,用于检查对象是否有必要的方法。

JavaScript模仿接口有三种方法:注释法、属性检查法和鸭式辩型法。三者结合使用可以达到令人满意的效果。

示例如下:

//定义接口

 

var Composite = new Interface('Composite',['add','remove']);
var FormItem = new Interface('FormItem',['save']);

 

//继承类 注释需要实现的接口

 

var CompositeForm = function(id,method,action){  //implements Composite,FormItem
}

 

//在需要调用的时候验证

 

function addForm(formInstance){  
              Interface.ensureImplements(formInstance,Composite,FormItem);//如果未实现接口所有方法,throw Error
}

 

//接下来看看Interface的定义

 

function Interface(name, methods) {
    if(arguments.length != 2) {
        throw new Error("Interface constructor called with " + arguments.length + "arguments, but expected exactly 2.");
    }
    this.name = name;
    this.methods = [];
    for(var i = 0, len = methods.length; i < len; i++) {
        if(typeof methods[i] !== 'string') {
            throw new Error("Interface constructor expects method names to be " + "passed in as a string.");
        }
        this.methods.push(methods[i]);
    }
};
// Static class method.
Interface.ensureImplements = function(object) {
    if(arguments.length < 2) {
        throw new Error("Function Interface.ensureImplements called with " + arguments.length + "arguments, but expected at least 2.");
    }
    for(var i = 1, len = arguments.length; i < len; i++) {
        var interface = arguments[i];
        if(interface.constructor !== Interface) {
            throw new Error("Function Interface.ensureImplements expects arguments" + "two and above to be instances of Interface.");
        }
        for(var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) {
            var method = interface.methods[j];
            if(!object[method] || typeof object[method] !== 'function') {
                throw new Error("Function Interface.ensureImplements: object "  + "does not implement the " + interface.name + " interface. Method " + method + " was not found.");
            }
        }
    }
};
 

 

接口类使用场合

在运用设计模式实现复杂系统时才能体现其价值。比如在很多人参与的大型项目中,更侧重于代码的可维护性,需要对JavaScript语法的灵活性做一些限制,那么接口可以起到重要作用。

在一些需要使用但还未编写完成的API中,用接口标记API需要实现的方法,在API实现后或者更新后,只要满足接口,就可以替换原有的代码。

此外,在利用网上API开发程序时,为每个API创建Interface对象,对接收到的API实例进行检查,确保程序所需方法都已经实现避免调用出错。

依赖于接口的设计模式

工厂模式,组合模式,装饰器模式,命令模式

 

--------------------------------------------第三章 封装和信息隐藏------------------------------------------------------------------

 

封装

JavaScript没有对封装提供内置的支持,需要采用语言本身的特性和技巧来模拟实现。

JavaScript创建对象方法有三种方式

    门户大开型(只提供公用成员方法)

    命名规范私有成员(下划线前缀标记的方法为私有。编码约定,无法实现有效的访问控制)

    闭包(利用闭包创建真正的私有成员)

此外还可以创建静态属性和常量属性:

    静态方法和属性     即创建类级别的方法和属性

    常量                 在JavaScript中通过创建只有取值器的私有变量来模仿常量。这里需要在闭包中创建私有变量,并创建一个公共的取值器方法。

 

用闭包实现私有成员导致的派生问题被称为‘继承破坏封装’(inheritance breaks encapsulation)

如果你创建的类以后可能还需要派生子类,那么最好还是采用 ‘命名规范私有成员‘方法。因为创建的子类无法访问超类闭包空间内的方法

(虽然在Java等语言中子类也无法访问父类的私有方法,但可以访问父类的受保护域方法)

在YUI等大型面向对象的JavaScript框架中,有继承关系的对象也是采用 ‘命名规范私有成员‘实现私有方法。

 

封装之利

封装保护了内部数据的完整,通过限制对内部的访问,减少对对象的破坏操作;另外,对象的重构因此变得轻松,可以随心所欲的改变内部的数据结构和算法。

通过封装,可以弱化模块间的耦合,提高对象独立性,可重用性。同时也避免了命名冲突。

封装之弊

单元测试困难,所有对象语言都存在这个问题。

封装意味着不得不与复杂的作用域链打交道,使错误调试更加困难。使用私有方法和属性所需的闭包导致调试困难。

如果你想使用这些模式,那么必须确保与你合作的其他程序员也理解它们

 

--------------------------------------------第四章 继承------------------------------------------------------------------

 

继承的好处是减少重复性代码,但会建立对象间的耦合关系,一个类依赖于另一个类的内部实现。(使用掺元类为其他类提供方法的技术有助于避免这种问题)

类式继承与继承链

JavaScript是可以被装扮成使用类式继承的语言。通过使用’构造器‘函数声明类,用关键字new创建实例。

继承链是围绕JavaScript中Function对象的prototype属性及prototype的constructor属性实现

(JavaScript中每个对象都有一个原型对象,但不是每个对象都有prototype属性,只有函数对象才拥有。在非ie浏览器下,原型对象可访问,obj.__proto__)

类式继承复杂性仅限于类的声明,创建实例还是一样的简单。

为了简化类声明,可以把派生子类的过程包装在函数中-extend函数。

function extend(subclass,superclass){

    var F = function(){};

    F.prototype = superclass.prototype;

    subclass.prototype = new F();

    subclass.prototype.constructor = superclass;

}

使用空函数F的实例作为子类的原型加入原型链的好处:避免实例化超类或者传入超类的实例,因为它可能比较庞大,并且超类的构造函数或许对子类有一定的副作用。

这样在继承的时候可以这样使用:

function Parent(name){this.name=name;}

Parent.prototype.getName = function(){return this.name;};

 

function Child(name,age){

    Parent.call(this,name);

    this.age = age;

}

extend(Child,Parent);

不足之处是父类被固化在子类的‘构造器’中,造成耦合。

因此更普适性的方法实现extend函数是如下方法:

 

function extend(subclass,superclass){
    var F = function(){};
    F.prototype = superclass.prototype;
    subclass.prototype = new F();
    subclass.prototype.constructor = superclass;

    subclass.super = superclass.prototype;
    if(superclass.prototype.constructor==Object.prototype.constructor){
        superclass.prototype.constructor = superclass;
    }
}
 

 

提供‘super’属性弱化父子类的耦合;子类‘构造器’就可以如下定义了:

 

function Child(name,age){
   Child.super.constructor.call(this,name);
   this.age = age;
}

 注:如果感觉Child里面使用Child标记不舒服,可以使用arguments.callee

 

 

function Child(name,age){
    arguments.callee.super.constructor.call(this,name);
    this.age = age;
}
 

 

原型式继承

原型式继承与类式继承截然不同,一切从对象的角度考虑

原型继承不需要类,直接创建一个对象即可。这个对象随后可以被新的对象重用(这得益于原型链查找的工作机制),这个对象被称为原型对象(prototype Object)

,因为它为其他对象应有的摸样提供了一个原型。

采用原型继承重新创建Parent 和 Child  :

 

var Parent  = {

  name:'p',

  getName:function(){..}

};

var Child = {

    age:12,

    getAge:function(){}

};

Child.__proto__ = Parent;   //非IE浏览器下有效。__proto__即指向Child对象的原型对象的指针。

alert(Child.name);//  'p'

接下来采用一种跨浏览器的方式实现原型继承,需要创建对象克隆的一个函数clone

 

function clone(obj){     //返回以给定对象为原型的空对象
   var F = function(){};
   F.prototype = obj;
   return new F();
}
 //父对象
var Parent  = {
  name:'p',
  getName:function(){}
};
//克隆父对象生成子对象并设置子对象属性和方法
var Child = clone(Parent);
Child.age = 12;
Child.gift = ['bike','car'];
//同样,可以继续克隆Child的子对象
var Child1 = clone(Child);
Child1.age = 13;
Child1.gift = [];
 

 

以上是基于原型继承来直接创建对象。但要特别注意的是继承而来的属性的读和写的不对等性:

再次克隆一个子类Child2

var Child2 = clone(Child);

Child2.age = 14;

Child2.gift.push('plane');

alert(Child2.age);//14

alert(Child2.gift);//'bike,car,plane'

然后打印父对象Child的属性:

alert(Child.age);//12

alert(Child.gift);//'bike,car,plane'   //!!!

运行后会看到父对象的引用类型属性也在子对象修改的时候被修改了。这里涉及到属性的读写特性:

对于值类型属性,修改只会影响对象本身;对于引用类型属性,如果对象本身未设置,那么读取的将是原型对象的属性。

所以为了避免修改引用类型属性造成的原型链上游的父对象被修改,需要在建立子对象的时候为引用类型属性创建副本。

如下:Child2.gift = [];这样修改Child2的gift属性就不会影响父对象。

这一点也同样需要在使用类式继承的时候注意,一般引用的属性在继承的时候都会重新创建。

 

类式继承与原型继承比较

差别是创建类(对象)以及派生子对象(实例)的方式不同。

原型继承更节约内存;克隆的对象都共享每个属性和方法的唯一实例,只有直接设置了某克隆对象的属性和方法时,情况才有所变化。

 

 

掺元类-mix

有一种重用代码的方法不需要用到严格的继承。通过扩充(augmentation)的方式让这些类共享该函数

具体实现大体为:先创建一个包含各种通用方法的类,然后再用它扩充其他类。

这种包含通用方法的类称为掺元类(mixin class)。它们通常不会被实例化或直接调用。

这可以被视为‘多亲继承’在JavaScript中的一种实现方式。要知道,在JavaScript中是不允许一个子类继承多个超类的。(一个对象只能有一个原型对象)

由于一个类可以用多个掺元类加以扩充,所以也就实际上实现了多继承的效果。但要注意未改变原型链。

实现扩展的方法比较简单,建立一个扩充类的函数augment即可。可以参考下YUI3中的Y.augment函数

 

掺元类适用于组织那些彼此迥然不同的类所共享的方法。实现在各类中通用方法的共享。

 

三种继承方式比较

在内存效率重要的场合采用原型式继承(及clone函数)是最佳的选择。

如果与对象打交道的是只熟悉面向对象由于继承机制的程序员,最好采用类式继承(及extend函数)。

这两种方式都适合于类间差异较小的类层次体系(hierarchy)。

如果类之间差异比较大,那么用掺元类(augment函数)方法来扩充这些类是更合理的选择。

 

最后说明,在简单的JavaScript程序中很少用到这种程度的抽象。只有那些许多程序员参与的大型项目才需要这种代码组织手段。

 

 

 

--------------------------------------------第五章 单体模式------------------------------------------------------------------

 

单体模式在JavaScript中有很多用途。划分命名空间,以减少全局变量数目;还可以在一种名为分支(branching)的技术中封装浏览器的差异。

还可以把代码组织得更为一致,便于阅读和维护。

1,最简单的单体结构

 

var Singleton = {
    attribute1:true,
    attribute2:10,
    method1:function(){},
    method2:function(){}
};
Singleton.method1();

 严格来说这并不算单体,在JavaScript中所有对象都是易变的,无法阻止对象被修改。如果某些变量需要保护,可以采用之前介绍的闭包方法。

 

给单体一个更广义的定义,单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果它可以被实例化,只能被实例化一次。

一般它是用来组织一批相关方法和属性,而不是模仿关联数组或数据容器。所以主要区别在于设计者的意图。

2,划分命名空间

命名空间是可靠的JavaScript编程的一个重要工具。由于JavaScript中什么都可以被改写,一不留神会擦除一个变量、函数甚至整个类,这种错误查找起来非常费时。

而且,现在的页面加载的JavaScript代码来源不止一个(自己的代码、库代码等),通过命名空间进行有效的组织,避免命名冲突,也可以尽快锁定问题域。

MyNamespace.Singleton = {};

3,特定页面的代码包装器

对于某个专门页面使用,其他页面不适用的代码,最好与公用的单体分开,将它包装在自己的单体对象中。

4,私有成员单体

第一种是使用下划线命名规范定义私有成员需要注意一点,在公有方法中调用私有方法,一般我们会使用this,但由于单体对象常用做提供共享的方法,所以在作为事件处理方法时需要进行作用域校正,

一种避免麻烦的方法是不使用this,而是使用它的命名空间标记(MyNamespace.Singleton._method)

另一种方法就是借助闭包。(该单体模式又称为‘模块模式’)

闭包的问题是每生成一个实例就会再次创建构造器内的方法和属性,由于单体只被实例化一次,不必付出什么代价,所以不用担心这个问题会带来空间损耗。

该类单体模式也是JavaScript中最流行应用最广泛的模式之一。

 

MyNamespace.Singleton = (function(){
    var _private;
   return {//publish ..
      method1:function(){}
   };
})();

 5,惰性实例化

 

前面的单体对象都是在脚本加载的时候就被创建出来。对于资源密集型或配置开销大的单体,更合理的做法是推迟到需要的时候实例。(惰性加载-lazy loading)

常用在加载大量数据的单体,对于作为命名空间与组织相关方法的单体还是立即实例化。

在Java中,单例实现有多种,其中实现懒加载的方式(懒汉式)是在进行方法调用的时候判断是否实例化。

在JavaScript中同样也需要借助静态方法调用:Singleton.getInstance().method1();而不是 Singleton.method1();

实现也比较简单:(由于结构变得稍微复杂,最好给予足够的注释标识为什么如此实现)

 

MyNamespace.Singleton = (function(){
    function constructor(){
        var _private;
        return {//publish
            method1:function(){}
        };
    }
    return {
        getInstance:function(){
            //if(..)  return  constructor();
        }
    };
})();

 6,分支

 

分支(branching)是一种用来把 浏览器的差异性 封装在运行期间进行设置的 动态方法 中的技术。

例如创建一个返回XHR对象的方法,需要进行浏览器嗅探或对象探测,如果不用分支技术,那么每次调用这个方法,都会进行嗅探代码的运行,缺乏效率。

更有效的做法是在脚本加载时一次性的确认针对特定浏览器的代码。这样初始化完成后,每次执行都只会执行特定的代码。提高调用执行的效率。

这种在运行时动态确定函数代码的能力,正是JavaScript高度灵活性和强大表现力的一种体现。

示例:

 

MyNamespace.Singleton = (function(){
    var obja = {
        method:function(){}
    };
    var objb = {
        method:function(){}
    };
    return ()?obja:objb;
})();
 

 

单体模式使用场合

在为代码提供命名空间和增强其模块性的时候尽量多的使用单体模块。

对开销较大很少使用的组件可以使用惰性加载单体。

对针对特定环境的代码可以包装到分支型单体中。

 

单体模式之利

好处在于对代码的组织作用,使调试和维护变得轻松;同时具有描述性的命名空间也利于阅读和理解;而且代码隔离作用也避免了被误改,提高了程序稳定性

单体模式的一些高级变体(惰性加载、分支技术)可以在开发后期对脚本进行优化。

单体模式之弊

由于单体模式的单点访问,有可能导致模块间的强耦合。也不利于单元测试。

所以单体还是在定义命名空间和实现分支方法最适合。

有时某种更高级的模式更符合任务的需要,与惰性加载单体相比,虚拟代理能给予你对实例化方式更多的控制权。

不要仅仅因为单体‘够可以了’就选择它,应该确保选择的模式适合自己的任务。

 

单体模式小结

单体即可单独使用,也可以与其他很多模式配合使用。例如对象工厂被设计为单体,组合对象的所有子对象也可以封装在一个单体命名空间中。

寻找组织和说明代码的各种方法是如何创建可重用的模块化代码的重要步骤之一。

 

 

 

--------------------------------------------第六章 方法的链式调用--------------------------------------------------------------

 

链式调用只不过是一种语法招数。 通过重用一个初始操作来达到少量代码表达复杂操作的目的。(一般采用return this;来实现)

在JQuery使用中会比较常见。其他框架也经常会使用。如YUI3 Node的创建及属性操作。(参考博客文章【JavaScript链式调用小结】)

链式调用一般适用于赋值器方法。如果你想在取值器方法中也保持一致的链式调用,可以使用回调技术返回取值结果。

 

 

分享到:
评论
4 楼 Troland 2013-07-05  
houfeng0923 写道
Troland 写道
function extend(subclass,superclass){
    var F = function(){};
    F.prototype = superclass.prototype;
    subclass.prototype = new F();
    subclass.prototype.constructor = superclass;

    subclass.super = superclass.prototype;
    if(superclass.prototype.constructor==Object.prototype.constructor){
        superclass.prototype.constructor = superclass;
    }
}

LZ:这里面最后一句。。为什么要这样写?超类的原型构造函数本身不就是他自己吗?为什么还做这个样子的判断?我测试了一下函数的原型构造函数就是他自己啊。。真纳闷。还望帮解疑。。谢谢了


你好。这个应该是对一种编程方式的兼容处理。如下:
var superCls = function(){
  this.name = '李天一';
}
superCls.prototype = {
   sex:function(){}
}

此时 如果将 superCls传入 extend,代码就会执行到 最后的判断了。
你这么有兴趣,相信也明了了吧。:)

谢谢大神,我测试了一下。发现只要函数的原型设置写成fn.prototype = {}这样的形式,最近又在看jQuery源码。突然想起来为什么他还设置那constructor的原因原来是在这样的啊。。再次感谢。。
3 楼 houfeng0923 2013-07-04  
Troland 写道
function extend(subclass,superclass){
    var F = function(){};
    F.prototype = superclass.prototype;
    subclass.prototype = new F();
    subclass.prototype.constructor = superclass;

    subclass.super = superclass.prototype;
    if(superclass.prototype.constructor==Object.prototype.constructor){
        superclass.prototype.constructor = superclass;
    }
}

LZ:这里面最后一句。。为什么要这样写?超类的原型构造函数本身不就是他自己吗?为什么还做这个样子的判断?我测试了一下函数的原型构造函数就是他自己啊。。真纳闷。还望帮解疑。。谢谢了


你好。这个应该是对一种编程方式的兼容处理。如下:
var superCls = function(){
  this.name = '李天一';
}
superCls.prototype = {
   sex:function(){}
}

此时 如果将 superCls传入 extend,代码就会执行到 最后的判断了。
你这么有兴趣,相信也明了了吧。:)
2 楼 Troland 2013-07-04  
function extend(subclass,superclass){
    var F = function(){};
    F.prototype = superclass.prototype;
    subclass.prototype = new F();
    subclass.prototype.constructor = superclass;

    subclass.super = superclass.prototype;
    if(superclass.prototype.constructor==Object.prototype.constructor){
        superclass.prototype.constructor = superclass;
    }
}

LZ:这里面最后一句。。为什么要这样写?超类的原型构造函数本身不就是他自己吗?为什么还做这个样子的判断?我测试了一下函数的原型构造函数就是他自己啊。。真纳闷。还望帮解疑。。谢谢了
1 楼 Troland 2013-07-04  
这篇文章写得真好。。

相关推荐

Global site tag (gtag.js) - Google Analytics