【类和模块】子类
子类
在面向对象编程中,类B可以继承自另外一个类A。我们将A称为父类(superclass), 将B称为子类(subclass)。B的实例从A继承了所有的实例方法。类B可以定义自己的实例方法,有些方法可以重载类A中的同名方法,如果B的方法重载了A中的方法,B中的重载方法可能会调用A中的重载方法,这种做法称为“方法链” (method chaining)。同样,子类的构造函数
B()有时需要调用父类的构造函数A(),这种做法称为“构造函数链” (constructor chaining)。子类还可以有子类,当涉及类的层次结构时,往往需要定 义抽象类(abstract class) 。抽象类中定义的方法没有实现。抽象类中的抽象方法是在抽象类的具体子类中实现的。
在JavaScript中创建子类的关键之处在于,采用合适的方法对原型对象进行初始化。如果类B继承自类A,B.prototype必须是A.prototype的后嗣。B的实例继承自B.prototype,后者同样也继承自A.prototype。
定义子类
JavaScript的对象可以从类的原型对象中继承属性(通常继承的是方法)。如果O是类B的实例,B是A的子类,那么O也一定从A中继承了属性。为此,首先要确保B的原型对象继承自A的原型对象。通过
inherit()函数,可以这样来实现:B.prototype = inherit(A.prototype); // 子类派生自父类 B.prototype.constructor = B; // 重载继承来的constructor属性
这两行代码是在JavaScript中创建子类的关键。如果不这样做,原型对象仅仅是一个普通对象,它只继承自Object.prototype,这意味着你的类和所有的类一样是Object的子类。如果将这两行代码添加至
defineClass()函数中,可以将它变成下例中的defineSubclass()函数和Function. prototype.extend()方法:例:定义子类 // 用一个简单的函数创建简单的子类 function defineSubclass(superclass, // 父类的构造函数 constructor, // 新的子类的构造函数 methods, // 实例方法:复制至原型中 statics) // 类属性:复制至构造函数中 { // 建立子类的原型对象 constructor.prototype = inherit(superclass.prototype); constructor.prototype.constructor = constructor; // 像对常规类一样复制方法和类属性 if (methods) extend(constructor.prototype, methods); if (statics) extend(constructor, statics); // 返回这个类 return constructor; } // 也可以通过父类构造函数的方法来做到这一点 Function.prototype.extend = function (constructor, methods, statics) { return defineSubclass(this, constructor, methods, statics); };
上例展示了不使用
defineSubclass()函数如何“手动”实现子类。这里定义了Set的子类SingletonSet。 SingletonSet是一个特殊的集合,它是只读的,而且含有单独的常量成员。
例:SingletonSet:—个简单的子类 // 构造函数 function SingletonSet(member) { this.member = member; // 记住集合中这个唯一的成员 } // 创建一个原型对象,这个原型对象继承自Set的原型 SingletonSet.prototype = inherit(Set.prototype); // 给原型添加属性 // 如果有同名的属性就覆盖Set.prototype中的同名属性 extend(SingletonSet.prototype, { // 设置合适的constructor属性 constructor: SingletonSet, // 这个集合是只读的:调用add()和remove()都会报错 add: function () { throw "read-only set"; ), remove: function () { throw "read-only set"; ), // SingletonSet的卖例中永远只有一个元素 size: function () { return 1; }, // 这个方法只调用一次,传入这个集合的唯一成员 foreach: function (f, context) { f.call(context, this.member); }, // contains()方法非常简单:只须检査传入的值是否匹配这个集合唯一的成员即可 contains: function (x) { return x === this.member; } });
这里的SingletonSet类是一个比较简单的实现,它包含5个简单的方法定义。它实现了5个核心的Set方法,但从它的父类中继承了
toString()、toArray()和equals()方法。定义子类就是为了继承这些方法。比如,Set类的equals()方法用来对Set实例进行比较,只要Set的实例包含size()和foreach()方法,就可以通过equals()比较。因为SingletonSet是Set的子类,所以它自动继承了equals()的实现,不用再实现一次。当然,如果想要最简单的实现方式,那么给SingletonSet类定义它自己的equals()版本会更高效一些:SingletonSet.prototype.equals = function(that) { return that instanceof Set && that.size()==1 && that.contains(this.member);
};
需要注意的是,SingletonSet不是将Set中的方法列表静态地借用过来,而是动态地从Set类继承方法。如果给
Set.prototype添加新的方法,Set和SingletonSet的所有实例就会立即拥有这个方法(假定SingletonSet没有定义与之同名的方法)。
构造函数和方法链
定义子类时,我们往往希望对父类的行为进行修改或扩充,而不是完全替换掉它们。为了做到这一点,构造函数和子类的方法需要调用或链接到父类构造函数和父类方法。
下例对此做了展示。它定义了Set的子类NonNullSet,它不允许null和undefined作为它的成员。为了使用这种方式对成员做限制,NonNullSet需要在其
add()方法中对null和undefined值做检测。但它需要完全重新实现一个add()方法,因此它调用了父类中的这个方法。注意,NonNullSet()构造函数同样不需要重新实现,它只须将它的参数传入父 类构造函数(作为函数来调用它,而不是通过构造函数来调用),通过父类的构造函数来初始化新创建的对象。例:在子类中调用父类的构造函数和方法 /* * NonNullSet是Set的子类,它的成员不能是null和undefined */ function NonNullSet() { // 仅链接到父类 // 作为普通函数调用父类的构造函数来初始化通过该构造函数调用创建的对象 Set.apply(this, arguments); } // 将NonNullSet设置为Set的子类 NonNullSet.prototype = inherit(Set.prototype); NonNullSet.prototype.constructor = NonNullSet; // 为了将null和undefined排除在外,只须重写add()方法 NonNullSet.prototype.add = function() { // 检査参数是不是null或undefined for (var i = 0; i < arguments.length; i++) if (arguments[i] == null) throw new Error("Can't add null or undefined to a NonNullSet"); // 调用父类的add()方法以执行实际插入操作 return Set.prototype.add.apply(this, arguments); };
让我们将这个非null集合的概念推而广之,称为“过滤后的集合”,这个集合中的成员必须首先传入一个过滤函数再执行添加操作。为此,定义一个类工厂函数,传入一个过滤函数,返回一个新的Set子类。实际上,可以对 此做进一步的通用化的处理,定义一个可以接收两个参数的类工厂:子类和用于
add()方法的过滤函数。这个工厂方法称为filteredsetSubclass(),并通过这样的代码来使用它:// 定义一个只能保存字符串的“集合”类 var StringSet = filteredSetSubclass(Set, function(x) (return typeof x==="string";}); // 这个集合类的成员不能是null、undefined或函数 var MySet = filteredSetSubclass (NonNullSet, function(x) (return typeof x !== "function";});
下例是这个类工厂函数的实现代码。注意,这个例子中的方法链和构造函数链和NonNullset中的实现是一样的。
例:类工厂和方法链 /* * 这个函数返回具体Set类的子类 * 并重写该类的add()方法用以对添加的元素做特殊的过滤 */ function filteredSetSubclass(superclass, filter) { var constructor = function() { // 子类构造函数 superclass.apply(this, arguments); // 调用父类构造函数 }; var proto = constructor.prototype = inherit(superclass.prototype); proto.constructor = constructor; proto.add = function() { // 在添加任何成员之前首先使用过滤器将所有参数进行过滤 for (var i = 0; i < arguments.length; i++) { var v = arguments[i]; if (!filter(v)) throw ("value " + v + " rejected by filter"); } // 调用父类的add()方法 superclass.prototype.add.apply(this, arguments); }; return constructor; }
上例中一个比较有趣的事情是,用一个函数将创建子类的代码包装起来,这样就可以在构造函数和方法链中使用父类的参数,而不是通过写死某个父类的名字来使用它的参数。也就是说如果想修改父类,只须修改一处代码即可,而不必对每个用到父类类名的地方都做修改。已经有充足的理由证明这种技术的可行性,即使在不是定义类工厂的场景中,这种技术也是值得提倡使用的。
最后,值得强调的是,类似这种创建类工厂的能力是JavaScript语言动态特性的一个体现,类工厂是一种非常强大和有用的特性,这在Java和C++等语言中是没有的。
组合vs子类
定义的集合可以根据特定的标准对集合成员做限制,而且使用了子类的技术来实现这种功能,所创建的自定义子类使用了特定的过滤函数来对集合中的成员做限制。父类和过滤函数的每个组合都需要创建一个新的类。
然而还有另一种更好的方法来完成这种需求,即面向对象编程中一条广为人知的设计原则:“组合优于继承”。这样,可以利用组合的原理定义一个新的集合实现,它“包装”了另外一个集合对象,在将受限制的成员过滤掉之后会用到这个(包装的)集合对象。下例展示了其工作原理:
例:使用组合代替继承的集合的实现 /* * 实现一个FilteredSet,它包装某个指定的”集合”对象, * 并对传入add()方法的值应用了某种指定的过滤器 * “范围”类中其他所有的核心方法延续到包装后的实例中 */ var FilteredSet = Set.extend( function FilteredSet(set, filter) { // 构造函数 this.set = set; this.filter = filter; }, { // 实例方法 add: function() { // 如果已有过滤器,直接使用它 if (this.filter) { for (var i = 0; i < arguments.length; i++) { var v = arguments[i]; if (!this.filter(v)) throw new Error("FilteredSet: value " + v + " rejected by filter"); } } // 调用set中的add()方法 this.set.add.apply(this.setarguments); return this; }, // 剩下的方法都保持不变 remove: function() { this.set.remove.apply(this.set, arguments); return this; }, contains: function(v) { return this.set.contains(v); }, size: function() { return this.set.size(); }, foreach: function(f, c) { this.set.foreach(f, c); } });
在这个例子中使用组合的一个好处是,只须创建一个单独的FilteredSet子类即可。可以利用这个类的实例来创建任意带有成员限制的集合实例。比如,不用上文中定义的NonNullSet类,可以这样做:
var s = new FilteredSet(new Set(), function(x) { return x !== null; });甚至还可以对已经过滤后的集合进行过滤:
var t = new FilteredSet(s, { function(x) { return !(x instanceof Set); }};
类的层次结构和抽象类
不管是在经典的面向对象编程语言中还是在JavaScript中,通行的解决办法是“从实现中抽离出接口”。假定定义了一个AbstractSet类,其中定义了一些辅助方法比如toString(),但并没有实现诸如foreach()的核心方法。这样,实现的Set、SingletonSet 和FilteredSet都是这个抽象类的子类,FilteredSet和SingletonSet都不必再实现为某个不相关的类的子类了。
下例在这个思路上更进一步,定义了一个层次结构的抽象的集合类。AbstractSet只定义了一个抽象方法:
contains()。任何类只要“声称”自己是一个表示范围的类,就必须至少定义这个contains()方法。然后,定义AbstractSet的子类AbstractEnumerableSet。 这个类增加了抽象的size()和foreach()方法,而且定义了一些有用的非抽象方法 (toString()、toArray()、equals()等),AbstractEnumerableSet并没有定义add()和remove()方法,它只代表只读集合。SingletonSet可以实现为非抽象子类。最后,定义了AbstractEnumerableSet的子类AbstractWritableSet。这个final抽象集合定义了抽象方法add()和remove(),并实现了诸如union()和intersection()等非具体方法,这两个方法调用了add()和remove()。AbstractWritableSet是Set和FilteredSet类相应的父类。但这 个例子中并没有实现它,而是实现了一个新的名叫ArraySet的非抽象类。
下例中的代码很长,但还是应当完整地阅读一遍。注意这里用到了
Function.prototype.extend()作为创建子类的快捷方式。
例:抽象类和非抽象Set类的层次结构 // 这个函数可以用做任何抽象方法,非常方便 function abstractmethod() { throw new Error("abstract method"); } /* * AbstractSet类定义了一个抽象方法:contains() */ function AbstractSet() { throw new Error("Can't instantiate abstract classes"); } AbstractSet.prototype.contains = abstractmethod; /* * NotSet是AbstractSet的一个非抽象子类 * 所有不在其他集合中的成员都在这个集合中 * 因为它是在其他集合是不可写的条件下定义的 * 同时由于它的成员是无限个,因此它是不可枚举的 * 我们只能用它来检测元素成员的归属情况 * 注意,我们使用了Function.prototype.extend()方法来定义这个子类 */ var NotSet = AbstractSet.extend( function NotSet(set) { this.set = set; }, { contains: function (x) { return !this.set.contains(x); }, toString: function (x) { return "~" + this.set.toString(); }, equals: function (that) { return that instanceof NotSet && this.set.equals(that.set); } } }; /* * AbstractEnumerableSet是AbstractSet的一个抽象子类 * 它定义了抽象方法size()和foreach() * 然后实现了非抽象方法isEmpty()、toArray()、 to[Locale]String()和equals()方法 * 子类实现了contains()、size()和foreach(),这三个方法可以很轻易地调用这5个非抽象方法 */ var AbstractEnumerableSet = AbstractSet.extend( function () { throw new Error("Can't instantiate abstract classes"); }, { size: abstractmethod, foreach: abstractmethod, isEmpty: function () { return this.size() == 0; }, toString: function () { var s = "{", i = 0; this.foreach(function (v) { if (i++>o) s += ", "; s += v: }); return s + "}"; }, toLocaleString: function () { var s = "{", i = 0; this.foreach(function (v) { if (i++>0) s += ","; if (v == null) s += v; // null和undefined else s += v.toLocaleString(); // 其他的情况 }); return s + ")"; }, toArray: function () { var a =[]; this.foreach(function (v) { a.push(v);}); return a; }, equals: function (that) { if (! (that instanceof AbstractEnumerableSet)) return false; // 如果它们的大小不同,则它们不相等 if (this.size() != that.size()) return false; // 检査每一个元盘是否也在that中 try { this.foreach(function (v) { if (!that.contains(v)) throw false; }); return true; // 所有的元素都匹配:集合相等 } catch(x) { if (x === false) return false; // 集合不相等 throw x; // 发生了其他的异常:重新抛出异常 } } }); /* * SingletonSet是AbstractEnumerableSet的非抽象子类 * singleton集合是只读的,它只包含一个成员 */ var SingletonSet = AbstractEnumerableSet.extend( function SingletonSet(member) { this.member = member; }, { contains: function (x) { return x === >this.member; }, size: function () { return 1; }, foreach: function (f, ctx) { f.call(ctx,this.member); } } ); /* * AbstractWritableSet是AbstractEnumerableSet的抽象子类 * 它定义了抽象方法add()和remove() * 然后实现了非抽象方法union()、intersection()和 difference() */ var AbstractWritableSet = AbstractEnumerableSet.extend( function () { throw new Error("Can't instantiate abstract classes");}, { add: abstractmethod, remove: abstractmethod, union: function (that) { var self = this; that.foreach(function (v) { self.add(v); }); return this; }, intersection: function (that) { var self = this; this.foreach(function (v) { if (!that.contains(v)) self.remove(v);}); return this; }, difference: function (that) { var self = this; that.foreach(function (v) {self.remove(v);}); return this; } }); /* * ArraySet是AbstractWritableSet的非抽象子类 * 它以数组的形式表示集合中的元素 * 对于它的contains()方法使用了数组的线性査找 * 因为contains()方法的算法复杂度是O(n)而不是O(1) * 它非常适用于相对小型的集合,注意,这里的实现用到了ES5的数组方法indexOf()和forEach() */ var ArraySet = AbstractWritableSet.extend( function ArraySet() { this.values = []; this.add.apply(this, arguments); }, { contains: function (v) { return this.values.indexOf(v) 1= -1; ), size: function () { return this.values.length; }, foreach: function (f, c) { this.values.forEach(f, c); }, add: function () { for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; if (!this.contains(arg)) this.values.push(arg); } return this; }, remove function () { for(var i = 0; i < arguments.length; i++) { var p = this.values.indexOf(arguments[i]); if (p == -1) continue; this.values.splice(p, 1); } return this; } } );