概述
JS虽然并不直接具备面向对象的特性,但仍可以通过prototype来模拟面向对象的继承和多态等特性。和大多数面向对象语言(例如C++,Java等)相比,JS来实现面向对象仍稍显繁琐和抽象,需要对JS的prototype模型有深入的理解。
在开发过程中,有时候会遇到这样一个问题:如果在子类中“覆盖”了超类的某个方法,但仍需要在子类方法中调用一次超类方法,这时候应该怎么做?如果是Java,一个简单的super关键字即可解决问题,但如果是JS呢?
解决问题的最基本方法可以是:在子类中,使用超类类型,通过apply关键字,以当前类实例引用执行一次超类方法。如下:
定义类A
// 定义类A
function A(a) {
this.a = a;
}
// 为类A定义show方法
A.prototype.show = function() {
alert("A: " + this.a);
};
定义类B并从A继承
// 定义类B
function B(a, b) {
// 调用A的构造函数
A.apply(this, arguments);
this.b = b;
}
// 链接A的原型
B.prototype = new A();
实例化类B对象并调用show方法
var b = new B(100, 200);
b.show();
此时,会执行定义在A中的show方法(显示A.a的值),表示类B已经从类A中继承了show方法
类B覆盖show方法
// 覆盖show方法
B.prototype.show = function() {
A.prototype.show.apply(this, arguments);
alert("B: " + this.b);
};
// 执行覆盖后的方法
b.show();
在B的prototype中重新定义show方法,即可以认为B类覆盖了A类的show方法。注意 A.prototype.show.apply(this, arguments) 这一句,实际上是利用了JS的原型特性,在B类对象中(以B类对象)执行了一次A类的show方法。
JS特殊的动态语言特性使得“覆盖”这个语义可以随时发生,甚至可以通过操作prototype来取消”覆盖“语义,这也正是JS的灵活和强大之处。
更进一步
通过上面的例子,可以发现,JS的”继承“,”覆盖“和”调用超类方法“虽然不难理解,但写起来仍较为繁琐,下面的代码可以简化这个流程。(以下代码灵感部分来源于ExtJS库,部分参考自prototype库)
namespace
// 定义根命名空间
ALV = {};
// 定义注册命名空间的方法
ALV.namespace = function(ns) {
var root = window;
var parts = ns.split(".");
for (var i = 0; i < parts.length; i++) {
var p = parts[i];
if (!root[p]) {
root[p] = {};
}
root = root[p];
}
};
合并对象的apply方法
// 合并对象成员
ALV.apply = function(obja, objb, def) {
if (def) {
ALV.apply(obja, def);
}
if (obja && objb && typeof objb === 'object') {
for (var o in objb) {
obja[o] = objb[o];
}
}
};
定义类的方法
// 原型定义
ALV.define = function(clazz, config) {
var parts = clazz.split(".");
var root = window;
for (var i = 0; i < parts.length - 1; i++) {
root = root[parts[i]];
}
var cn = parts[parts.length - 1];
if (!root[cn]) {
root[cn] = function() {};
}
clazz = root[cn];
// 将proto对象的成员赋值给类的原型
ALV.apply(clazz.prototype, config);
return clazz;
};
定义子类并继承超类的方法
// 定义继承的方法
ALV.extend = function(base, child, proto) {
// 将超类原型赋值给类的原型
var c = ALV.define(child);
if (base && typeof base === "function") {
c.prototype = new base();
}
if (proto && typeof proto == "object") {
ALV.apply(c.prototype, proto);
}
// 调用超类方法
c.prototype.callParent = function(args) {
var m;
for (var o in this) {
if (this[o] === this.callParent.caller) {
m = o;
}
}
var method = base.prototype[m];
if (method && typeof method === "function") {
method.apply(this, args);
}
};
};
上述代码中,子类的 prototype 链接到了超类对象上,完成了 prototype 的继承,而 callParent 方法中,通过对当前类调用方法的查找,找到方法名(m变量),再在超类的 prototype 中找到同名方法,利用超类方法的 apply 操作,在子类对象上完成对超类方法的调用。
测试代码
// 定义命名空间
ALV.namespace("Alvin.test");
// 定义超类
ALV.define("Alvin.test.A", {
a: 100,
show: function() {
alert("A: " + this.a);
}
});
// 定义子类
ALV.extend(Alvin.test.A, "Alvin.test.B", {
a: 100,
b: 200,
show: function() {
this.callParent(arguments);
alert("B: " + this.b);
}
});
// 实例化B类对象
var b = new Alvin.test.B();
b.show();
从测试代码中可以看到,Alvin.test.B 类继承了 Alvin.test.A 类,且覆盖了其中的 show 方法,在 B 类的 show 方法中,this.callParent(arguments) 调用完成了对 A 类show方法的调用。这样 B 类就可以自然的访问超类中的指定方法而无需关心超类究竟使用什么名称。