子类
在面向对象编程中,类 B 可以继承自另一个类 A,我们将 A 称为父类,将 B 称为子类。B 的实例从 A 继承了所有的实例方法,类 B 也可以定义自己的实例方法,有些方法可以重载类 A 中的同名方法。如果 B 的方法重载了 A 的方法,B 中的重载方法可能会调用 A 中的重载方法,这种做法称为「方法链」,同样,子类的构造函数有时需要调用父类的构造函数,这种做法称之为「构造函数链」。子类还可以有子类,当涉及类的层次结构时,往往需要定义抽象类。抽象类中定义的方法没有实现,需要在抽象类的具体子类中实现。
定义子类
我们可以这样来定义类的继承:
B.prototype = inherit(A.prototype);
B.prototype.constructor = B;
这两行代码是 JavaScript 中创建子类的关键,如果不这样做,原型对象只是继承自 Object.prototype
的普通对象,这意味着你的类和所有类一样都是 Object 的子类。我们将上面两行代码添加到 defineClass()
形成定义子类的方法:
/**
* 一个用以定义简单类的函数
* @param superclass 父类构造函数
* @param constructor 子类构造函数
* @param methods 实例方法,会被复制到原型对象中
* @param statics 类属性,会被复制到构造函数中
* @returns constructor
*/
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);
};
当然,我们还可以不使用构造函数手动来实现子类,下面我们就来定义 Set
类的子类 SingletonSet
,SingletonSet
是一个特殊的集合,是只读的,而且含有单独的常量成员:
// 定义构造函数
function SingletonSet(member) {
this.member = member; // 集合中的唯一成员
}
// SingletonSet 的原型对象继承自 Set 的原型对象
SingletonSet.prototype = inherit(Set.prototype);
// 给原型添加属性
// 如果属性同名则覆盖 Set.prototype 中的对应属性
extend(SingletonSet.prototype, {
constructor: SingletonSet,
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: function (x) {
return x === this.member;
}
});
我们来测试下上述代码:
除了在子类中重载的5个方法外,SingletonSet
还从父类中继承了 toString()
、toArray()
和 equals()
方法:
当然,我们也可以对 equals()
方法进行重写:
SingletonSet.prototype.equals = function (that) {
return that instanceof Set && that.size() == 1 && that.contains(this.member);
};
需要注意的是,SingletonSet
不是将 Set
类中的方法静态地借用过来,而是动态地继承方法。如果给 Set.prototype
添加新方法,Set
和 SingletonSet
的所有实例就会立即拥有这个方法(除非 SingletonSet
已经定义了同名方法)。
构造函数和方法链
当定义子类时,我们更希望对父类的行为进行修改或扩充,而不是完全替换,为了实现这个目的,构造函数和子类的方法需要调用或链接到父类构造函数和父类方法。为此,我们再定义一个 Set
的子类 NoNullSet
,它不允许 null
和 undefined
作为它的成员,所以需要在 add()
方法中对新增元素做校验,但是我们不需要完全定义一个新的 add()
方法,同理,构造函数也是如此:
function NoNullSet() {
// 调用父类的构造函数对子类进行初始化
Set.apply(this, arguments);
}
// 定义 NoNullSet 继承自 Set
NoNullSet.prototype = inherit(Set.prototype);
NoNullSet.prototype.constructor = NoNullSet;
// 重写 add 方法
NoNullSet.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 NoNullSet");
}
// 调用父类 add() 方法以执行实际插入操作
return Set.prototype.add.apply(this, arguments);
};
对上述代码进行测试以检测其是否能正常工作:
对这个示例进一步推而广之,可以定义出各种类型的过滤集合,比如字符串集合、对象集合、函数集合等。为此,我们可以定义一个工厂方法,传入一个过滤函数,返回一个新的 Set
子类,我们将这个方法命名为 filteredSetSubClass
:
/**
* 这个工厂方法返回具体 Set 类的子类
* 需要重写 add() 方法对添加的元素做特殊的过滤
* @param superclass
* @param filter
* @returns {constructor}
*/
function filteredSetSubClass(superclass, filter) {
var constructor = function () { // 子类构造函数
superclass.apply(this, arguments); // 调用父类构造函数
};
var proto = constructor.prototype = inherit(superclass.prototype);
proto.constructor = constructor;
proto.add = function () {
// 对所有新增元素调用 filter() 函数进行过滤
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 语言动态特性的一个体现,类工厂是一种非常强大和有用的特性。
组合 vs 子类
在前一节中,定义的集合可以根据特定的标准对集合成员做限制,而且使用了子类的技术来实现。父类和过滤函数的每个组合都要创建一个新的类来实现。
除了子类之外,还有一种更好的方法来完成这种需求,即面向对象中一种广为人知的设计原则:「组合优于继承」(这句话出自《设计模式》一书)。我们可以通过组合的原理定义一个新的集合实现,还是以一个具体的例子来演示:
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.set, arguments);
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
即可,可以通过这个子类来创建任何带有成员限制的集合实例。比如 NoNullSet
对象实例可以这样创建:
var noNullSet = new FilteredSet(new Set(), function (x) {
return x != null;
});
`StringSet` 对象实例可以这样创建:
var stringSet = new FilteredSet(new Set(), function (x) {
return typeof x == 'string';
});
新的组合类 FilteredSet
中包装了集合类 Set
,对组合实例方法的调用实际上是对包装的集合对象方法的调用。
类的层次结构和抽象类
本节我们将引入抽象类,抽象类和接口一样,都是从实现中抽离出接口,从而使类的层次结构更加清晰,同时提高系统的可扩展性和可维护性。
下面我们将以一个具体实例的方式来演示抽象类的使用,AbstractSet
类只定义了一个抽象方法 contains()
,然后定义 AbstractSet
的子类 AbstractEnumerableSet
,这个类增加了抽象的 size()
和 foreach()
方法,但并没有定义 add()
和 remove()
方法,代表只读集合,SingletonSet
是继承自它的非抽象子类,最后定义了 AbstractEnumerableSet
的子类 AbstractWritableSet
,这个抽象类定义了 add()
和 remove()
方法,我们定义了 ArraySet
子类继承自它:
/**
* 一个用以定义简单子类的函数
* @param superclass 父类构造函数
* @param constructor 子类构造函数
* @param methods 实例方法,会被复制到原型对象中
* @param statics 类属性,会被复制到构造函数中
* @returns constructor
*/
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;
}
// 把 p 中的可枚举属性复制到 o 中,并返回 o
function extend(o, p) {
for (prop in p) {
o[prop] = p[prop];
}
return o;
}
// 通过该方法定义继承自原型p的新对象
function inherit(p) {
if (p == null) throw TypeError();
if (Object.create)
return Object.create(p);
var t = typeof p;
if (t !== 'function' && t !== 'object') throw TypeError();
function f() {};
f.prototype = p;
return new f();
}
// 在父类上调用 extend() 方法来定义子类,下面会用到这种方式来定义子类
Function.prototype.extend = function (constructor, methods, statics) {
return defineSubClass(this, constructor, methods, statics);
};
// 这个函数可以用作任何抽象方法
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 () {
return "~" + this.set.toString();
},
equals: function (that) {
return that instanceof NotSet && this.set.equals(that.set);
}
}
);
/**
* AbstractEnumerableSet 是 AbstractSet 的抽象子类
* 定义了两个抽象方法 size() 和 foreach()
* 以及一些非抽象方法
*/
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++ > 0)
s += ", ";
s += v;
});
return s + "}";
},
toLocaleString: function () {
var s = "{", i = 0;
this.foreach(function (v) {
if (i++ > 0)
s += ", ";
if (v == null)
s += v;
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;
try {
this.foreach(function (v) {
if (!that.contains(v))
throw false;
});
} catch (x) {
if (x === false)
return false;
throw x;
}
return true;
}
}
);
/**
* SingletonSet 是 AbstractEnumerableSet 的非抽象子类
* 它是只读的,只包含一个属性
*/
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()
* 以及计算集合并集、交集、差集的非抽象方法
*/
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) {
if (that.contains(v))
self.remove(v);
});
return this;
}
}
);
/**
* ArraySet 是 AbstractWritableSet 的非抽象子类
* 它以数组的形式表示集合中的元素
* 我们以数组提供的方法和属性实现 contains、size 和 foreach 方法,这样效率更高
*/
var ArraySet = AbstractWritableSet.extend(
function ArraySet() {
this.values = [];
this.add.apply(this, arguments);
},
{
contains: function (v) {
return this.values.indexOf(v) !== -1;
},
size: function () {
return this.values.length;
},
foreach: function (f, ctx) {
this.values.forEach(f, ctx);
},
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;
}
}
);
下面我们来测试下上述代码:
No Comments