继承

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

为什么需要继承

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

类继承

通过用函数来声明类,用关键字 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();

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

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 中最基本的模式,它不仅可以单独使用,还能和大多数其他模式配合使用.
例如,对象工厂可以被设计为单体,组合对象的所有子对象也可以被封装进一个单体命名空间中.
本书讲的是如何创建可重用的模块化代码,单体对全局变量的减少具有重要作用.


南赐
125 声望4 粉丝

大多数情况下会在segmentfault上面活跃,日后主攻小程序,storybook + vue 组件开发,前端测试,持续集成,node.js,eggjs等等。期待与大家深入交流技术~


« 上一篇
封装