继承&单体

继承

在 js 中继承较为复杂,比其它语言的继承要复杂.在大多数面向对象语言中,继承一个类只要使用一个关键字便可,而 js 要传承公有成员的话须要使用灵活微妙的原型继承,或者标准的类继承.
本文第一部分将讨论 js 中建立子类的各类技术以及它们的使用场合.

为何须要继承

先看看继承能带来的好处.设计类的时候,但愿能减小重复性的代码,尽可能弱化对象间的耦合.使用继承符合前一个原则.能够在现有类的基础上进行设计并充分利用他们已经具有的各类方法.
让一个类继承另外一个类可能会致使两者产生强耦合,一个类依赖于另外一个类的内部实现.接下来会讲到如何避免.好比用掺元类为其余类提供方法...等等.css

类继承

经过用函数来声明类,用关键字 new 来建立实例,下面是一个简单的类声明:前端

// Class Person
function Person(name) {
    this.name = name;
}

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

首先要作的事建立构造函数,名字就是类名,首字母大写.在构造函数中,建立实例属性要使用关键字 this.类的方法则被添加到其 prototype 对象中.要建立该类的实例,只需结合关键字 new 调用这个构造函数:程序员

var reader = new Person('John Smith');
reader.getName();

而后你能够访问全部的实例属性,也能够调用全部的实例方法.数组

原型链

建立继承 Person 的类要复杂一些:浏览器

// Class Author
function Author(name, books) {
    Person.call(this, name); // Call the superclass's constructor in the scope of this.
    this.books = books; // Add an attribute to Author.
}

Author.prototype = new Person(); // Set up the person chain.
Author.prototype.constructor = Author; // Set the constructor attribute to Author.
Author.prototype.getBooks = function () { // Add to method to Author.
    return this.books;
}

让一个类继承另外一个类须要用到许多行代码(不像其余面向对象语言只要一个关键字 extend 便可),首先要作的是建立一个构造函数,在构造函数中,调用超类的构造函数.并将 name 参数传给他,在使用 new 运算符时,系统会为你作一些事,会建立一个空对象,而后调用构造函数,在此过程当中这个空对象处于做用域链的最前端.
下一步是设置原型链,js 没有 extend 关键字,可是每一个 js 对象中都有一个名为 prototype 的属性,要么指向另外一个对象,要么 Null.在访问对象的某个成员时(好比reader.getName),若是这个成员未见于当前对象,那么 js 会在prototype属性所指的对象中查找他,没找到js就会沿着原型链向上逐一访问每一个原型对象,直到找到他(或者已经查找过原型链最顶端的 Object.prototype 对象).
因此说为了让一个类继承另外一个类,只需将子类的 prototype 设置为指向超类的一个实例便可.
为了让Author 继承 Person,必须手动地将 Author 的 prototype 设置为 Person 的一个实例,最后一步是将 prototype 的 constructor 属性重设为 Author(由于把 prototype 属性设置为 Person 的实例时,其 constructor 属性被抹掉了).
尽管本例中为实现继承须要额外使用三行代码,可是建立这个新的子类的的实例与建立 Person 的实例没有什么不一样:闭包

var author = [];
author[0] = new Author('Dustin Diaz', ['Javascript Design Patterns']);
author[0] = new Author('Ross Harmes', ['Javascript Design Patterns']);

author[1].getName();
author[1].getBooks();

因此说,类式继承的复杂性只局限于类的声明,建立新实例的过程仍然很简单.app

extend 函数

为了简化类的声明,能够把派生子类的整个过程包装在一个名为 extend 的函数中,他的做用和其余语言的 extend 关键字相似,即基于一个给定的类结构建立一个新类:框架

// Extend Function.

function extend(subClass, superClass) {
    var F = function () {};
    F.prototype = superClass.prototype;
    subClass.prototype = new F();
    subClass.prototype.constructot = subClass;
}

这个 extend 函数和先前咱们作的同样,它设置了 prototype.而后再从新设置其 constructor 为其自己.有一项改进,他添加了空函数 F,并将用它建立的一个对象实例插入原型链中(这样能够避免建立超类的新实例).
使用了 extend 函数后:异步

// Class Person.
function Person(name) {
    this.name = name;
}

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

// Class Author.
function Author(name, books) {
    Person.call(this.name);
    this.books = books;
}

extend(Author, Person);

Author.prototype.getBooks = getBooks() {
    return this.books;
}

上面的代码不像以前那样手动地设置prototype 和 constructor 属性,而是经过在类声明以后(在向 prototype 添加任何方法以前)当即调用 extend 函数.惟一的问题是超类(Person)的名称被固化在 Author 类的声明之中,更好的作法是下面那样:模块化

// Extend function, improved.

function extend(subClass, superClass) {
    var F = function() {};
    subClass.prototype = new F();
    subClass.superclass = superclass.prototype;
    if (superClass.prototype.constructor == Object.prototype.constructor) {
        superClass.prototype.constructor = superClass;
    }
}

说到这个 改进版的extend函数,我想起了之前的一个东东,说要实现一个js类继承工具方法:

  1. 继承

  2. 多态
    如今想一想,此处的 extend 函数已经实现了第一步,还差第二步,暂不讨论.

该版本要长一点,可是提供了superclass 属性,这个属性用来弱化 Author 和 Person 之间的耦合.该函数的最后3行代码用来确保超类的 constructor 属性已被正确设置(即时超类就是 Object 类自己),在用这个新的superclass 属性调用超类的构造函数时这个问题很重要:

// Class Author.
function Author(name, books) {
    Author.superclass.constructor.call(this, name);
    this.books = books;
}
extend(Author, Person);

Author.prototype.getBooks = function () {
    return this.books;
};

有了 superclass 属性,就能够直接调用超类的方法,这在既要重定义超类的某个方法又想访问其在超类的实现时能够派上用场.例如,为了用一个新的 getName 方法重定义 Person 类中的同名方法,你能够先用Author.superclass.getName 得到做者名字,而后在此基础上添加其余信息:

Author.prototype.getName = function () {
    var name = Author.superclass.getName.call(this);
    return name + ',Author of' + this.getBooks(0.join(', ');
}

原型式继承

它与类式继承大相径庭,此刻最好忘掉类和实例的一切知识,只从对象的角度来思考.用基于类的办法来建立对象包括两个步骤:首先,用一个类的声明定义对象结构;第二,实例化该类以建立一个新对象.用这种方式建立的对象都有一套该类的全部实例属性的副本.每个实例方法都只存在一份,可是每一个对象都有一个指向他的连接.
使用原型式继承并不须要用类来定义对象的结构,只需直接建立一个对象便可.这个对象随后能够被新的对象重用,这个得益于原型链查找的工做机制.该对象被称为原型对象.取原型式继承这个名称是由于他为其余对象应有的模式提供了一个原型.
下面咱们使用原型式继承从新设计 Person 和 Author:

// Person Prototype Object.
var Person = {
    name: 'default name',
    getName: function () {
        return this.name;
    }
};

这里没有使用一个名为 Person 的构造函数来定义类的结构,Person如今是一个对象字面量.他是所要建立的其余各类类 Person 对象的原型对象.其中定义了全部类 Person 对象都要具有的属性和方法,而且有默认值.方法的默认值通常不会改变,可是属性与此相反.

var reader = clone(Person);
alert(reader.getName()); // This will output 'default name'.
reader.name ='John Smith';
alert(reader.getName()); // This will output 'John Smith'.

clone 函数能够用来建立新的类 Person 对象.他会建立一个空对象,二该对象的原型对象被设置成 Person.也就是说若是在这个新对象中查找不刀某个方法或者属性时,那么接下来会在其原型对象中继续查找.
没必要为建立A Author 而定义一个一个 Person 的子类,只需执行一次克隆便可.

// Author Prototype Object.
var Author = clone(Person);
Author.books = []; // Default value.
Author.getBooks = function () {
    return this.books;
}

而后你能够重定义该克隆中的方法和属性.能够修改在 Person 中提供的默认值.也能够添加新的属性和方法.这样一来就建立了一个新的原型对象.能够将其用于建立新的类 Author 对象:

var author = [];

author[0] = clone(Author);
author[0].name = 'Dustin Diaz';
author[0].books = ['Javascript Design Patterns'];

author[1] = clone(Author);
author[1].name = 'Ross Harmes';
author[1].books = ['Javascript Design Patterns'];

author[1].getName();
author[1].getBooks();

对继承而来的成员的读写不对等性

在类式继承中,Author 的每个实例都有一份本身的 books 数组副本,能够用代码 author[1].books.push('New Book Title')为其添加元素.可是对于使用原型式继承方式建立的类 Author 对象来讲,因为原型链的工做方式,这种作法行不通.一个克隆并不是其原型对象的一份彻底独立的副本,只是一个以那个对象为其原型对象的空对象而已.克隆刚被建立时,author[1].name 实际上是一个指向最初的Person.name 的连接,对于从原型对象继承而来的成员,其读和写具备内在的不对等性.在读取 author[1].name 的值时,若是尚未直接为 author[1]实例定义 name 属性的话,那么所获得的事其原型对象的同名属性值.而在写入 author[1].name 的值时,你是在直接为 author[1]对象定义一个新属性.下面这个实例显示了这种不对等性:

var authorClone = clone(Author);
console.log(authorClone.name); // Linked to the primative Person.name, which is the string 'default name'.
authorClone.name = 'new name';// A new primative is created and added to the authorClone object itself.
console.log(authorClone.name); // Now linked to the primative authorClone.name, which is the string 'new name'.

authorClone.books.push('new book'); // authorClone.books is linked to the arrayAuthor.books. We just modifiedthe prototype object's default value, and all other objects that link to it will now have a new default value there.
authorClone.books = []; // A new array is created andadded to the authorClone object itself.
authorClone.books.push('new book'); // We are now modifying that new array.

上面的例子说明了为何必须经过引用传递的数据类型的属性建立新副本.向 authorClone.books 数组添加新元素其实是把这个元素添加到Author.books 数组中,这样的话值的修改会同时影响到Author 和全部继承了 Author 但还未改写那个属性的默认值的对象.这种错误必须尽可能避免,调试起来会很是费时.在这类场合,可使用 hasOwnProperty 方法来区分对象的实际成员和继承而来的成员.
有时原型对象本身也含有子对象.若是想覆盖其子对象中的一个属性值,不得不从新建立整个子对象.这能够经过将该子对象设置为一个空对象字面.而后对其重塑.但这意味着克隆出来的对象必须知道其原型对象的每个子对象的确切结构.和默认值.为了尽可能弱化对象之间的耦合,任何复杂的子对象都应该使用方法来建立:

var ComponoudObject = {
    string1: 'default value',
    childObject: {
        bool: true,
        num: 10
    }
}

var CompoundObject = {
    string1: 'default value',
    childObject: {
        bool: true,
        num: 10
    }
}
var compoundObjectClone = clone(CompoundObject);

// Bad! Changes the value of CompoundObject.childObject.num.
compoundObjectClone.childObject.num = 5;
// Better. Creates a new object, but compoundObject must know the structure of that object, and the defaults. This makes CompoundObject and compoundObjectClone tightly coupled.
compoundObjectClone.childObject = {
    bool: true,
    num: 5
};

在这个例子中,为 compoundObjectClone 必须知道 childObject 具备两个默认值分别为 true 和10的属性.这里有一个更好的办法: 用工厂办法来建立 childObject:

// Best approach. Uses a method to create a new object, with the same structure and defaults as the original.

var CompoundObject = {};
CompoundObject.string1 = 'default value';
CompoundObject.createChildObject = function () {
    return {
        bool: true,
        num: 10
    }
};
CompoundObject.childObject = CompoundObject.createChildObject();

var compoundObjectClone = clone(CompoundObject);
compoundObjectClone.childObject = CompoundObject.createChildObject();
compoundObjectClone.childObject.num = 5;

clone 函数

以前的例子用来建立克隆对象的 clone 函数到底是什么样呢:

// Clone function.

function clone(object) {
    function F() {}
    F.prototype = object;
    return new F;
}

clone 函数首先建立了一个新的空函数 F,而后将 F 的 prototype 属性设置做为参数 object 传入的原型对象.prototype 属性就是用来指向原型对象的,经过原型链机制,它提供了到全部继承而来的成员的连接.该函数最后经过把 new 运算符做用于 F 建立出一个新对象.而后把这个新对象做为返回值返回.函数所返回的这个克隆结果是一个一给定对象为原型对象的空对象.

类式继承和原型式继承的对比

类式继承和原型式继承是截然不同的两种继承范型,他们生成的对象也有不一样的行为方式.须要对二者的优缺点和特定使用场合进行了解.
若是你设计的是一个众人使用的 API,或者可能会有不熟悉原型式继承的其余程序员基于你的代码进行改造.那么最好使用类式继承.

原型式继承更能节约内存.原型链读取成员的方式使得全部克隆出来的对象都共享每一个属性和方法的惟一一份实例,只有在直接设置了某个克隆出来的对象的属性和方法时,状况才会变化.

类式继承方式中建立的每个对象在内存中都有本身的一套属性(和私有方法)德芙笨.因此说原型式继承更节约内存,并且只使用一个 clone 函数也更为简练,不须要像后者那样须要为每个想继承的类写上好几行这样的晦涩代码:
`SuperClass.call(this, arg)和 SubClass.prototype = new SuperClass...`
不过也能够写到 extend 方法里面去,因此说最后到底使用哪一种继承方式除了考虑实际状况以外还取决于你的口味.

继承和封装

如今来谈谈封装对继承的影响.
从现有的类派生出一个子类时,只有公有和特权成员会被继承下来,可是私有成员没法继承下来.
因为这个缘由,门户大开型类是最适合派生子类的,它们的全部成员都是公开的,能够被遗传给子类,若是某个成员须要稍加隐藏,可使用下划线规范.
在派生具备真正的私有成员的类时,特权方法是公有的,因此会被遗传下来.因此能够在子类中间接访问父类的私有属性.可是子类的实例方法都不能直接访问这些私有属性.父类的私有成员只能经过这些既有的特权方法访问到,在子类中添加新特权方法也访问不到.

掺元类

这是一种没有严格继承,重用代码的方法.是这样,若是想把一个函数用到多个类中,能够经过扩充的方式让这些类共享该函数.

先建立一个包含各类通用方法的类,而后再用它扩充其余类,这种类叫作掺元类(mixin class),一般不会被实例化或者直接调用,只是向其余类提供本身的方法(说实话这个 mixin 在各类场合是否是很熟悉呢...各类 js 框架,css 预处理,是否是都跟这个有关呢...):

// Mixin class.
var Mixin = function () {};
Mixin.prototype = {
    serialize: function () {
        var output = [];
        for (key in this) {
            output.push(key + ';' + this[key]);
        }
        retyurn output.join(', ');
    }
};

这个 Mixin 类只有一个名为 serialize 的方法,遍历 this 对象的全部成员并输出一个字符串.这种方法可能在许多不一样类型的类中都会用到,可是没有必要让这些类都继承 Mixin,最好是用一个函数 augment 把这个方法添加到每个须要他的类中:

augment(Author, Mixin);

var author = new Author('Ross Harmes', ['Javascript Design Patterns']);
var serializaedString = author.serialize();

在此咱们用 Mixin 类中的全部方法扩充了 Author 类,Author 类的实例如今就能够调用 serialize 方法了,称为为多亲继承 multiple inheritance.尽管在 js 中一个对象只能用有一个原型对象,不容许子类继承多个超类,可是一个类能够用多个掺元类扩充,实际上也就实现了多继承.
augment 函数很简单,其实是用一个 for...in 循环遍历 第二个参数(Mixin 类,予类 giving class)的 prototype 中的每个成员,并将其添加到第一个参数(受类 receiving class)的 prototype 中,若是受类中已经存在同名成员,那么跳过它,这样受类中的成员就不会被改写.若是你想达到这么一个目的: 只复制掺元类当中的一两个方法,那么就能够给 augment 函数加上第三个及更多的可选参数:

// Augment function, improved.
function augment(receivingClass, givingClass) {
    if (arguments[2]) { // Only give certain methods.
        for (var i = 2, len = arguments.length; i < len; i++) {
            receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];
        }
    }
    else { // Give all methods.
        for (methodName in givingClass.prototype) {
            if (!receivingClass.prototype[methodName]) {
                receivingClass.prototype[methodName] = givingClass.prototype[methodName];
            }
        }
    }
}

如今使用 augment(Author, Mixin, 'serialize');z能够只为 Author 类添加一个 serialize 方法的目的了.
从条理性的角度来看,严格的继承方案比扩充方案更加清楚.掺元类很是适合于组织那些彼此迥然不一样的类所共享的方法.

小结

继承的好处主要表如今代码的重用方面.经过创建类或者对象之间的继承关系,有些方法咱们只需定义一次便可.若是须要修改这些方法或者排查其中错误,那么因为其定义只出如今一个位置,因此很是节省时间.

各类继承范型各有优缺点.

原型式继承工做机制: 先建立一些对象而后再对其进行克隆,从而获得建立子类和实例的等效效果.用这种办法建立的对象有很高的内存效率,由于它们会共享那些未被改写的属性和方法.

在内存效率重要的场合原型式继承(clone 函数)是最佳选择,若是你更容易接受其余面向对象语言中的继承机制,那么对于 js 继承仍是选用类式继承(extend 函数)比较好.这两种方法都适合于类间差别较小的类层次体系(hierarchy).
若是类之间的差别较大,那么用掺元类来扩充这些类会更合理.

------------ 分割线 ----------

单体

也叫单例模式singleton,js 中最基本也最有用.将代码组织为一个逻辑单元,能够经过单一的变量进行访问.单体对象只存在一份实例,全部代码使用的都是一样的全局资源.
单体类在 js 中有许多用途,能够用来划分命名空间,减小网页中全局变量的数量.它们还能够在一种名为分支的技术中用来封装浏览器之间的差别(在使用各类经常使用的工具函数时就没必要再操心浏览器嗅探的事).
最重要的是,能够把代码组织的更为一致,可维护性提升.
在网页上使用全局变量有很大的风险,而用单体对象建立的命名空间是清除这些全局变量的最佳手段之一.

基本结构

这里先讨论最基本最简单的类型,一个对象字面量,把一批有必定关联的方法和属性组织在一块儿:

// Basic Singleton.
var Singleton = {
    attribute1: true,
    attribute2, 10,

    method1: function () {

    },
    method2: function (arg) {

    }
};

示例中,全部成员均可以经过变量 Singleton 访问.可使用圆点运算符.
这个单体对象能够被修改.能够为其添加新成员,也能够用 delete 运算符删除其现有成员.实际上违背了面向对象设计的一条原则:类能够被扩展,但不该该被修改(道理有点像 css classes 的增减).区别于其余面向对象语言js 中的全部对象都易变,若是某些变量须要保护,那么能够将其定义在闭包之中.
你可能注意到了,刚刚的示例并非单体,由于按照定义,单体是一个只能被实例化一次而且能够经过一个访问点访问的类,而这个例子不是一个可实例化的类.咱们能够把单体定义地更广义一些:
单体是一个能够用来划分命名空间并将一批相关方法和属性组织到一块儿的对象,若是能够被实例化,那么它只能被实例化一次.
并不是全部对象字面量都是单体,若是只是用来模仿关联数组或者容纳数据的话,那就不是单体;可是若是是用来组织一批相关方法和属性的话就有多是单体.

划分命名空间

单体对象有两部分: 包含着方法和属性成员的对象自身,还有用于访问它的变量.这个变量一般是全局性的,这个变量一般是全局性的,一遍在网页上任何地方都能直接访问到它所指向的单体对象.

虽然定义单体没必要是全局性的,可是它应该在各个地方都能被访问,由于单体对象的全部内部成员都被包装在这个对象中,因此它们不是全局性的.

因为这些成员只能经过这个单体对象变量进行访问,因此能够说它们被单对对象圈在了一个命名空间中.

// using a namespace.
var MyNameSpace = {
    findProduct: function(id) {
        ...
    },
    // Other methods can go there as well.
}

...

// Later in your page, another programmer adds...
var resetProduct = $('reset-product-button');
var findProduct = $('reset-product-button'); // NOthing was overwritten.

如今 findProduct 函数是MyNameSpace中的一个办法,他不会被全局命名空间中声明的任何新变量改写.该方法仍然能够从各个地方访问,可是如今调用方式不是 findProduct(id),而是 MyNameSpace.findProduct(id).

用做特定网页专用代码的包装器的单体

已经知道如何把单体做为命名空间使用,如今咱们在介绍单体的一个特殊用途.

有些 js 代码是一个网站中全部网页都要用到的,一般被存放在独立的文件中;有些代码则是某个网页专用的,不会被用到其余地方,能够把这两种代码分别包装到本身的单体对象中.

拥有私有成员的单体

以前咱们讨论过建立类的私有成员的作法,使用真正私有方法一个缺点在于它们比较耗费内存,由于每一个实例都具备方法的一份新副本,不过因为单体对象只会被实例化一次,因此定义真正的私有方法时不用考虑内存.不过咱们先谈谈更简单的建立伪私有成员的作法.

使用下划线

// DataParser singleton, coverts character delimited strings into arrays.

GaintCorp.DAtaParser = {
    // private methods.
    _stripWhitespace: function (str) {
        return str.replace(/\s+/, '');
    },
    _stringSplit: function(str, delimiter) {
        return str.splist(delimiter);
    },

    // Public method.
    stringToArray: function(str, delimiter, stripWS) {
        if (stripWS) {
            str = this._stripWhitespace(str);
        }
        var outputArray = this._stringSplit(str, delimiter);
        return outputArray;
    }
};

使用闭包

在单体对象中建立私有成员的第二种办法须要借助闭包.与以前建立真正私有成员的作法很是类似.
但也有重要区别.先前的作法是把变量和函数定义在构造函数体内(不使用 this 关键字),此外还在构造函数内定义了全部的特权方法并用 this 关键字使其可被外界访问.每生成一个该类的实例时,全部声明在构造函数内的方法和属性都会再次建立一份,可能会很是低效.
由于单体只会被实例化一次,因此构造函数内成员个数不是重点.每一个方法和属性都只会被建立一次,因此能够把它们都声明在构造函数内(位于同一个闭包内)

// Singleton as an Object Literal.
MyNamespace.Singleton = {};

// Singleton with Private Members, step 1.
MyNamespace.Singleton = function () {
    return {};
}();

上面两个 MyNamespace.Singleton 彻底相同.对于第二个,并无把一个匿名函数赋给 MyNamespce.Singleton而是返回一个对象再赋值.函数定义后的大括号是为了当即执行该函数.还能够像下面那样再套上一对圆括号.

如今大概能够知道,谈到单体,有两个关键词:闭包+大括号
再回顾一下,能够把公有成员添加到单体所返回的那个对象字面量:

//Singleton with Private Members, step 2.

MyNamespace.Singleton = (function () {
    return { // Public members.
        publicAttribute0: true,
        publicAttribute2: 99,

        publicMethod1: function () {
            ...
        }
    };
})();

使用闭包和使用一个对象字面量的区别在于:
对于前者,任何声明在匿名函数中(但不是在那个对象字面量中)的变量或者函数都只能被在同一个闭包中声明的其余函数访问.这个闭包在匿名函数结束执行后依然存在,因此在其中声明的函数和变量总能从匿名函数所返回的对象内部访问.

单体模式跟js模块化有必定关联,因此又称模块模式,意指他能够把一批相关方法和属性组织为模块并起到划分命名空间.

比较

如今咱们再也不为每一个私有方法名称的开头添加一个下划线,而是把这些方法定义在闭包中:

// DataParser singleton, converts character delimited strings into arrays.
// Now using true private methods.

CiantCorp.DataPraser = (function () {
    // Private attributes.
    var whitespaceRegex =/\s+/;

    // Private methods.
    function stripWhitespace(str) {
        return str.repalce(whitespaceRegex, '');
    }
    function stringSplit(str, delimiter) {
        return str.split(delimiter);
    }

    // Everything returned in the object literal is public, but can access the members in the closure created above.

    return {
        // Public method.
        stringToArray: function(str, delimiter, stringWS) {
        if (stringWS) {
            str = stripWhitespace(str);
        }
        var outputArray = stringSplit(str, delimiter);
        return outputArray;
        }
    };
})();
// Invoke the functio nand assign the returned object literal to GiantCorp.DataParser.

如今这些私有方法和属性能够直接用其名称访问,没必要在其前面加上 this.或者GaintCorp.DataParser,这些前缀只用于访问单体对象的公有成员.
单体相比于下划线表示法有几点优点:

  • 把私有成员放到闭包中能够确保其不会在单体对象以外被使用.

  • 能够任意改变对象实现细节,不破坏其余代码.

  • 还能够对数据进行保护和封装.

使用单体时,能够享受真正的私有成员带来的好处,单体类只会被实例化一次,能够节省内存.这是单体模式成为受欢迎,应用普遍的模式之一的缘由.

注意事项:公有成员和私有成员的声明语法不同,前者被声明在对象字面量内部然后者并非这样.私有属性必须用 var 声明,不然它将成为全局性的,私有方法是按 `function funcName (args) {...}` 这样的形式声明,在最后一个大括号以后不须要使用分号,公有属性和方法分别按照 attributeName: attributeValue 和 `methodName: function (args) {...}`这样的形式声明.若是后面还要声明别的成员的话,那么该声明的后面应该加上一个逗号.

惰性实例化

以前的单体模式的各类实现方式有一个共同点:单体对象都是在脚本加载时被加载出来,若是单体资源密集或者配置开销大,那么更合理的作法是将其实例化推迟到须要使用它的时候.被称为惰性加载,最经常使用于那些必须加载大量数据的单体.那些被用作命名空间,特定网页专用代码包装器,组织相关实用方法的工具的单体最好仍是当即实例化.
惰性加载单体的特别之处在于对他们的访问必须借助于一个静态方法.这样调用: Singleton.getInstance().methodName(),而不是这样调用: Singleton.methodName().getInstance()方法荟兼差单体是否已经被实例化,若是尚未,那么将建立而且返回实例.若是实例化过,那么它将返回现有实例.下面咱们从前面那个拥有真正私有成员的单体出发将普通单体转化为惰性加载单体(转化工做第一步是把单体的全部代码移到一个叫作 constructor 的方法中:

// General skeleton for a lazy loading singleton, step 1.

MyNamespace.Singleton = (function() {

    function constructor () { // All of the normal singleton code goes here.
        // Private members.
        var privateAttribute1 = false;
        var privateAttribute2 = [1, 2, 3];

        function privateMethod1 () {
            ...
        }
        function privateMethod2 () {
            ...
        }

        return  { // Public members.
            publicAttribute1: true,
            publicAttribute2: 2,

            publicMethod1: function () {
                ...
            }
        }
    }
})();

这个方法不能从闭包外部访问这是件好事,由于咱们想控制调用时机.公有方法 getInstance 就是要这么作,为了使其成为公有方法,只须要将其放到一个对象字面量中而且返回该对象便可:

// General skeleton for a lazy loading singleton, step 2.

MyNamespace.Singleton = (fucntion () {
    function constructor() { // All of the normal singleton code goes here.
        ...
    }

    return  {
        getInstance: function () {
            // Control code goes here.
        }
    }
})();

如今讨论如何编写控制实例化时机的代码.首先,必须知道该类是否被实例化过;其次,若是该类被实例化过,那么他须要掌握其实例的状况,以便能返回这个示例;要作到这两点,须要用到一个私有属性和已有的私有方法constructor:

// General skeleton for a lazy loading singleton, step 3.
MyNamespace.Singleton = (function () {
    var uniqueInstance; // Private attribute that holds the single instance.

    function constructor () { // All of the normal singleton code goes here.
        ...
    }

    return {
        getInstance: function () {
            if (!uniqueInstance) { // Intantiate only if the instance doesn't exist.
                uniqueInstance = constructor();
            }
            return uniqueInstance;
        }
    }
})();

惰性加载单体缺点在于复杂性,用于建立这种类型的单体代码并不直观,不易理解.若是你须要建立一个延迟加载实例化的单体,那么最好为其写注释,以避免别人把其简化为普通单体.

分支

一种用来将浏览器间的差别封装到在运行期间进行设置的动态方法中的技术.假设咱们须要建立一个返回 XHR 对象的方法,这个XHR对象在大多数浏览器中是 XMLHttpRequest 类的实例,而在 IE 早期版本中则是某种 ActiveX 类的实例.咱们要建立的方法一般会进行某种浏览器嗅探或者对象检测.若是不用分支技术,那么每次调用时,全部那些浏览器嗅探代码都要再次运行.若是调用频繁,那么会很低效.
更有效的作法是只在脚本加载时一次性地肯定针对特定浏览器的代码,遮掩的话,在初始化后,每种浏览器都会只执行针对他的 js 实现而设计的代码.可以在运行时动态肯定函数代码的能力,是 js 的高度灵活性和强大表现能力的一种体现,提升了调用这些函数的效率.
在以前,单体对象的全部代码都是在运行时肯定的,这在鄙薄建立私有成员的模式中很容易看出来:

MyNamespace.Singleton = (function () {
    return {};
})();

这个匿名函数在运行时执行,返回的对象字面量赋值给 MyNamespace.Singleton 变量.

示例: 建立 XHR 对象

如今咱们要建立一个单体,他有一个用来生成 XHR 对象实例的方法.
首先判断分支数量,由于全部实例化的对象只有3种不一样类型,因此须要3个分支,分别按照其返回的XHR 对象类型命名:

// SimpleXhrFactory singleton.
var SimpleXhrFactory = (function () {

    // Three branches.
    var standard = {
        createXhrObject: function () {
            return new XMLHttpRequest();
        }
    };
    var activeXNew = {
        createXhrObject: function () {
            return new ActiveXObject('Msxml2.XMLHTTP');
        }
    };
    var activeXOld = {
        createXhrObject: function () {
            return new ActiveXObject('Microsoft.XMLHTTP');
        }
    };

    // To assign the branch, try each method,
    var testObject;
    try {
        testObject = standard.createXhrObject();
        return standard; // Return this if no error was thrown.
    }
    catch(e) {
        try {
            testObject = activeXNew.createObject();
            return activeNew;
        }
        catch(e) {
            try {
                testObject = activeXOld.createXhrObject();
                return activeXOld;
            }
            catch(e) {
                throw new Error('No XHR object found in the environment.');
            }
        }
    }

})();

上面的示例代码建立了三个对象字面量,它们有相同一个方法 createXhrObject(),它是用来返回一个能够执行异步请求的新对象,很明显名字虽然同样,方法内部代码不同,分支之间做出选择的判断条件值是在运行时肯定.这种条件一般是某种能力检测的结果,目的在于确保运行代码的 js 环境准确地实现了所须要的条件特性.
本例中,具体的条件判断步骤是这样的: 使用 try{...} catch{...} 来逐一尝试每种 XHR 对象,直到遇到一个当前 js 环境所支持的对象为止.

使用该 API,只要调用SimpleXhtFacyory.createXhtObject();就能获得适合特定的运行时环境的 XHR 对象.用了分支技术,全部那些特性嗅探代码只会执行一次,不是每生成一个对象就要执行一次.

单体的使用场合

使用单体,一则,提供命名空间,二则,加强其模块性.
单体模式几乎适用于全部大大小小的项目,在简单快开发的项目中,能够只把单体用做命名空间,将本身的全部代码组织在一个全局变量名下;在稍大稍复杂的项目中,把单体用来把相关代码组织在一块儿以便往后维护;在大型项目中:那些开销较大却不多使用的组件能够被包装到惰性加载单体中,而针对特定环境的代码能够被包装到分支型单体中.
几乎全部项目都会用到某种形式的单体,js 的灵活性使得单体能够被用于多种不一样任务,它在 js 当中的重要性大大超过他在其余语言中的重要性.由于它能够用来建立命名空间以减小全局变量的数目.因为全局变量在 js 中很容易被其余人重写,因此至关危险,单体模式能够很好的解决这种问题.

主要好处在于对代码的组织.
把相关方法和属性组织在一个不会被屡次实例化的单体中,可使得代码的调试和维护更轻松.单体能够把你的代码和第三方库代码,广告代码哥离开,提升网页的稳定性.
单体的一些高级变体能够在开发周期的后期用于对脚本进行优化,提高性能.
惰性实例化,能够直到须要一个对象的时候才建立它,从而减小哪些不须要他的用户承受的没必要要的内存消耗.
分支技术能够根据运行时条件肯定赋给单体变量的对象字面量,建立出为特定环境量身定制的方法,不会在每次调用时都一再浪费时间去检查运行环境.

主要的客观缺点:
单体提供的是一种单点访问,因此可能致使模块间强耦合,不利于单元测试.没法单独测试一个调用了来自单体的方法的类,只能把她与那个单体做为一个单元一块儿测试.
而对于划分命名空间,实现分支型方法这些用途,耦合不是什么问题.
有时候某些其余更高级的模式比单体高级变体更符合任务须要.
虚拟代理与惰性加载单体,能够给予你对类实例化方式更多的控制;还能够是用一个对象工厂来取代分支型单体.

小结

做为 js 中最基本的模式,它不只能够单独使用,还能和大多数其余模式配合使用.例如,对象工厂能够被设计为单体,组合对象的全部子对象也能够被封装进一个单体命名空间中.本书讲的是如何建立可重用的模块化代码,单体对全局变量的减小具备重要做用.

相关文章
相关标签/搜索