1. 继承机制实例
1.1. 说明继承机制最简单的方式是, 利用一个经典的例子: 几何形状。实际上, 几何形状只有两种, 即椭圆形和多边形。圆是椭圆的一种, 它只有一个焦点。三角形、矩形和五边形都是多边形的一种, 具有不同数量的边。正方形是矩形的一种, 所有的边等长。这就构成了一种完美的继承关系。
1.2. 在这个例子中, 形状(Shape)是椭圆形(Ellipse)和多边形(Polygon)的基类(base class所有类都由它继承而来)。圆形(Circle)继承了椭圆形, 因此圆形是椭圆形的子类(subclass), 椭圆形是圆形的超类(superclass)。同样, 三角形(Triangle)、矩形(Rectangle)和五边形(Pentagon)都是多边形的子类, 多边形是它们的超类。最后, 正方形(Square)继承了矩形。
2. 继承机制的实现
2.1. 要用JavaScript实现继承机制, 您可以从要继承的基类入手。所有开发者定义的类都可作为基类。出于安全原因, 本地类和宿主类不能作为基类, 这样可以防止公用访问编译过的浏览器级的代码, 因为这些代码可以被用于恶意攻击。
2.2. 选定基类后, 就可以创建它的子类了。 有时, 你可能想创建一个不能直接使用的基类, 它只是用于给子类提供通用的函数, 在这种情况下, 基类被看作抽象类。
2.3. 创建的子类将继承超类的所有属性和方法, 包括构造函数及方法的实现。记住, 所有属性和方法都是公用的, 因此子类可直接访问这些方法。子类还可添加超类中没有的新属性和方法, 也可以覆盖超类的属性和方法。
3. 对象冒充
3.1. 其原理如下: 构造函数使用this关键字给所有属性和方法赋值(即采用类声明的构造函数方式)。因为构造函数只是一个函数, 所以可使ClassA构造函数成为ClassB的方法, 然后调用它。ClassB就会收到ClassA 的构造函数中定义的属性和方法。例如, 用下面的方式定义ClassA和ClassB:
function ClassA(color) {
this.color = color;
this.sayColor = function () {
alert(this.color);
};
}
function ClassB(color) {}
3.2. 这个原理是把ClassA作为常规函数来建立继承机制, 而不是作为构造函数。如下使用构造函数ClassB可以实现继承机制:
function ClassB(color) {
this.newMethod = ClassA;
this.newMethod(color);
delete this.newMethod;
}
3.3. 在这段代码中, 为ClassA赋予了方法newMethod。然后调用该方法, 传递给它的是ClassB构造函数的参数color。最后一行代码删除了对ClassA的引用, 这样以后就不能再调用它。
3.4. 所有新属性和新方法都必须在删除了新方法的代码行后定义。否则, 可能会被超类的相关属性和方法覆盖:
function ClassB(color, name) {
this.newMethod = ClassA;
this.newMethod(color);
delete this.newMethod;
this.name = name;
this.sayName = function () {
alert(this.name);
};
}
3.5. 对象冒充可以实现多重继承
3.5.1. 有趣的是, 对象冒充可以支持多重继承。也就是说, 一个类可以继承多个超类。用UML表示的多重继承机制如下图所示:
3.5.2. 例如, 如果存在两个类ClassX和ClassY, ClassZ想继承这两个类, 可以使用下面的代码:
function ClassZ() {
this.newMethod = ClassX;
this.newMethod();
delete this.newMethod;
this.newMethod = ClassY;
this.newMethod();
delete this.newMethod;
}
3.5.3. 这里存在一个弊端, 如果存在两个类ClassX和ClassY具有同名的属性或方法, ClassY具有高优先级。因为它从后面的类继承。除这点小问题之外, 用对象冒充实现多重继承机制轻而易举。
4. call()方法
4.1. call()方法是与经典的对象冒充方法最相似的方法。它的第一个参数用作this的对象。其他参数都直接传递给函数自身。例如:
function sayColor(prefix, suffix) {
alert(prefix + this.color + suffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.call(obj, "The color is ", "a very nice color indeed.");
4.2. 在这个例子中, 函数sayColor()在对象外定义, 即使它不属于任何对象, 也可以引用关键字this。对象obj的color属性等于blue。调用call()方法时, 第一个参数是obj, 说明应该赋予sayColor()函数中的this 关键字值是obj。第二个和第三个参数是字符串。它们与sayColor()函数中的参数prefix和suffix匹配, 最后生成的消息"The color is blue, a very nice color indeed."将被显示出来。
4.3. 要与继承机制的对象冒充方法一起使用该方法, 只需将前三行的赋值、调用和删除代码替换即可:
function ClassA(color) {
this.color = color;
this.sayColor = function () {
alert(this.color);
};
}
function ClassB(color, name) {
//this.newMethod = ClassA;
//this.newMethod(color);
//delete this.newMethod;
ClassA.call(this, color);
this.name = name;
this.sayName = function () {
alert(this.name);
};
}
4.4. 这里, 我们需要让ClassA中的关键字this等于新创建的ClassB对象, 因此this是第一个参数。第二个参数color对两个类来说都是唯一的参数。
5. apply()方法
5.1. apply()方法有两个参数, 用作this的对象和要传递给函数的参数的数组。例如:
function sayColor(prefix, suffix) {
alert(prefix + this.color + suffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.apply(obj, new Array("The color is ", "a very nice color indeed."));
5.2. 这个例子与前面的例子相同, 只是现在调用的是apply()方法。调用apply()方法时, 第一个参数仍是obj, 说明应该赋予sayColor()函数中的this关键字值是obj。第二个参数是由两个字符串构成的数组, 与sayColor() 函数中的参数prefix和suffix匹配, 最后生成的消息仍是"The color is blue, a very nice color indeed.", 将被显示出来。
5.3. 该方法也用于替换前三行的赋值、调用和删除新方法的代码:
function ClassB(color, name) {
//this.newMethod = ClassA;
//this.newMethod(color);
//delete this.newMethod;
ClassA.apply(this, new Array(color));
this.name = name;
this.sayName = function () {
alert(this.name);
};
}
5.4. 同样的, 第一个参数仍是this, 第二个参数是只有一个值color的数组。可以把ClassB的整个arguments对象作为第二个参数传递给apply()方法:
function ClassB(color, name) {
//this.newMethod = ClassA;
//this.newMethod(color);
//delete this.newMethod;
ClassA.apply(this, arguments);
this.name = name;
this.sayName = function () {
alert(this.name);
};
}
5.5. 当然, 只有超类中的参数顺序与子类中的参数顺序完全一致时才可以传递参数对象。如果不是, 就必须创建一个单独的数组, 按照正确的顺序放置参数。
6. 原型链(prototype chaining)
6.1. prototype对象是个模板, 要实例化的对象都以这个模板为基础。总而言之, prototype对象的任何属性和方法都被传递给那个类的所有实例。原型链利用这种功能来实现继承机制。
6.2. 如果用原型方式重定义前面例子中的类, 它们将变为下列形式:
function ClassA() {}
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
alert(this.color);
};
function ClassB() {}
ClassB.prototype = new ClassA();
6.3. 这里, 把ClassB的prototype属性设置成ClassA的实例。
6.4. 调用ClassA的构造函数, 没有给它传递参数。这在原型链中是标准做法。要确保构造函数没有任何参数。
6.5. 与对象冒充相似, 子类的所有属性和方法都必须出现在prototype属性被赋值后, 因为在它之前赋值的所有方法都会被删除。为什么?因为prototype属性被替换成了新对象, 添加了新方法的原始对象将被销毁。所以, 为 ClassB类添加name属性和sayName()方法的代码如下:
function ClassB() {}
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function () {
alert(this.name);
};
6.6. 原型链的弊端是不支持多重继承。记住, 原型链会用另一类型的对象重写类的prototype属性。
7. 对象冒充和原型链
7.1. 对象冒充的主要问题是必须使用构造函数方式, 构造函数会重复生成函数。不过如果使用原型链, 不能通过给构造函数传递参数来初始化属性的值, 而且对象被多个实例共享。
7.2. 继承机制, 用对象冒充继承构造函数的属性, 用原型链继承prototype对象的方法。用这两种方式重写前面的例子, 代码如下:
function ClassA(color) {
this.color = color;
}
ClassA.prototype.sayColor = function () {
alert(this.color);
};
function ClassB(color, name) {
ClassA.call(this, color);
this.name = name;
}
ClassB.prototype = new ClassA();
ClassB.prototype.sayName = function () {
alert(this.name);
};
7.3. 在此例子中, 在ClassB构造函数中, 用对象冒充继承ClassA类的color属性。用原型链继承ClassA类的方法。
7.4. 由于这种混合方式使用了原型链, 所以instanceof运算符仍能正确运行。
8. 对象冒充例子
8.1. 代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>对象冒充实现继承</title>
</head>
<body>
<script type="text/javascript">
function Polygon(myName) { // 多边形
this.myName = myName;
}
function Rectangle(myName, width, height) { // 矩形
this.extendPolygon = Polygon;
this.extendPolygon(myName);
delete this.extendPolygon;
this.width = width;
this.height = height;
this.perimeter = function() {
return (this.width + this.height) * 2;
}
}
function Triangle(myName, width, height) { // 三角形
Polygon.call(this, myName);
this.width = width;
this.height = height;
this.area = function() {
return this.width * this.height / 2;
}
}
function Square(myName, width) { // 正方形
Rectangle.apply(this, [myName, width, width]);
this.area = function() {
return this.width * this.width;
}
}
var rectangle = new Rectangle('矩形', 100, 30);
document.write(rectangle.myName + '的周长: ' + rectangle.perimeter() + '<br />');
var square = new Square('正方形', 100);
document.write(square.myName + '的周长: ' + square.perimeter() + '<br />');
document.write(square.myName + '的面积: ' + square.area() + '<br />');
var triangle = new Triangle('三角形', 100, 30);
document.write(triangle.myName + '的面积: ' + triangle.area() + '<br />');
document.write((rectangle instanceof Polygon) + '<br />');
document.write((square instanceof Polygon) + '<br />');
document.write((triangle instanceof Polygon) + '<br />');
</script>
</body>
</html>
8.2. 效果图
9. 原型链例子
9.1. 代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>原型链实现继承</title>
</head>
<body>
<script type="text/javascript">
function ClassA() {}
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
document.write(this.color + '<br />');
};
function ClassB(){}
ClassB.prototype = new ClassA();
var personZhang = new ClassB();
personZhang.sayColor();
function ClassC(firstName, lastName){
this.firstName = firstName;
this.lastName = lastName;
}
ClassC.prototype.fullName = function() {
return this.firstName + this.lastName;
};
function ClassD(firstName, lastName){
ClassC.apply(this, [firstName, lastName]);
}
ClassD.prototype = new ClassC();
ClassD.prototype.drink = function() {
document.write(this.fullName() + '喝的多。' + '<br />');
};
var personLi = new ClassD('李', '四');
personLi.drink();
document.write((personLi instanceof ClassD) + '<br />');
document.write((personLi instanceof ClassC) + '<br />');
</script>
</body>
</html>
9.2. 效果图