对象的继承,指的是在原有对象的基础上进行修改,得到一个新的对象。新对象具有原对象的全部或部分功能,同时还可以具有一些原对象所没有的功能,但同时新对象不会影响原有对象的功能。其中,原对象称为父对象,新对象称为子对象。当子对象中某些功能和父对象完全相同时,可以直接使用父对象中的同功能的代码,而不需要重新定义。可见,对象的继承可以实现代码重用。事实上,对象继承是一个很常用的代码重用方式。
在 JavaScript 中,对象的继承有多种模式,比如通过原型链继承、通过借用构造函数实现继承、组合原型链和借用构造函数继承、复制继承、原型继承、寄生式继承以及寄生组合式继承等。
在子类调用父类,然后将父类删掉,仅留下父类执行后的结果。
面向对象开发的巨大好处就在于对象可以重复使用, OOP 编程包含两个步骤:对象的抽象和对象的使用,这两个步骤的成果都可以重复使用。
当对象被抽象为类时,可以使用继承关系重复使用类。但是,当两个不同类之间没有继承关系时,就可以把类实例化为个体,一个类的个体可以在其它的类中被多次使用,并成为其它类的组成部分。当个体与其它类有部件或组合关系时,就被称为组合。
在编程中通常需要这样一些类,这些类与其它现有的类拥有相同的属性和方法。实际上,定义一个通用类,用于所有的项目,并且不断丰富这个类以适应每个具体项目,将是一个不错的体验。
为了使这一点变得更加容易,类可以从其它的类中扩展出来。扩展或派生出来的类拥有其基类的所有属性和方法(这称为继承),并包含所有在派生类中定义的部分,类中的元素不可能减少,也就是说,不可以注销任何存在的方法或者属性。
继承可以避免重复编写相同的代码,因此十分有用。如果有两个单独的类,而每个类都必须实现 firstName 和 lastName 属性,则可能会出现重复代码。如果要更改某个属性的实现方式,则需要查找已实现这些属性的所有类以进行更改。这不仅要耗费大量时间,还增加了不同类中出现错误的风险。
在考虑使用继承时,有一点需要注意,那就是两个类之间的关系应该是" 属于" 关系。例如, Employee 是一个人, Manager 也是一个人,因此这两个类都可以继承 Person 类。但是 Leg 类却不能继承 Person 类,因为腿并不是一个人。
所有的类都直接或间接地继承于 Object 类,该类是根类。
在面向对象的编程中,通过继承创建的新类称为子类或派生类,被继承的类称为基类、父类或超类。子类可以继承另一个类的属性和方法,要继承一个类,可以使用 prototype 属性指定该类的一个实例,例如,下面创建一个新类,命名为 Child ,该类可以继承 Person 类,那么 Child 类可以继承 Person 类中的 nickName 和 age 属性以及 showInfo() 方法:
function Person(myName, myAge) {
this.age = myAge;
this.nickName = myName;
this.showInfo = function () {
return '我的名字是 ' + this.nickName + ' ,我现在 ' + this.age + '$1 了。 ';
};
}
function Child() {} // 一个新类
Child.prototype = new Person(); //继承 Person 类
下面来使用一下子类:
var child_1 = new Child();
child_1.nickName = 'Jane';
child_1.age = 8;
console.log(child_1.showInfo());
如果创建 Child 类的实例是要为其属性赋初始值该怎么办?例如下面的代码要实现的功能:
var child_1 = new Child('Jane', 8);
注意到在定义 Child 类时没有使用带参数的构造器方法,即使是使用了带参数的构造器也无法将参数传递给基类,这意味着必须使用下面的方法才能实现与基类 Person 相同的为属性赋初始值功能:
function Child(myName, myAge) {
this.nickName = myName;
this.age = myAge;
}
Child.prototype = new Person();
这就可以实现为实例初始化属性,但显然这不是继承了基类 Person 的属性,而是自己定义了属性。
要想不在 Child 类重新定义属性而实现向基类构造器方法传递参数,很多开发语言都定义了 super() 方法以实现该功能,但是 JavaScript 没有定义 super 方法,所以,可以使用下面的方法实现这样的功能:
function Child(myName, myAge) {
/**
* 在这里实现调用构造器方法,首先是获得基类的构造器
*
* 将它作为当前类的一个属性
*
*/
this.$super = Person;
/**
* 注意不能直接使用 super ,因为它是一个保留的关键字
*
* 然后调用基类的构造器
*/
//
this.$super(myName, myAge);
}
Child.prototype = new Person(); // 继承 Person 类
可以使用下面的方法检测一个子类是否继承于某个基类:
console.log(Child.prototype.constructor == Person);
上述代码检测子类原型的构造器方法是否与基类相同。
假定 GrandChild 是 Person 的一个更低级别的类(子类的子类,孙类),那么可以使用下面的方法来检测它们是否继承自同一个基类:
console.log(GrandChild.prototype.__proto__ == Child.prototype);
console.log(GrandChild.prototype.prototype == Child.prototype);
使用 prototype 和 __proto__
属性都可以让孙类的原型链上移,从而 GrandChild.prototype.__proto__
其实是上移了两步原型链。
如果一个类从另一类中继承而来,那么基类中的属性和方法在子类中都有效,即使在子类中没有声明。
就像以前提到过的,继承是非常强大的,如果想访问一个继承的属性,只需要像访问基类自己的属性那样引用即可,很多语言都定义了 super 方法来提供该功能, JavaScript 没有提供该功能,所以我们就自定义一个实现来完成该功能,这个实现就是 $super 方法。
$super 方法的行为类似于引用当前类的直接基类的成员变量,它常用于访问在派生类中被重写或隐藏的基类成员。由于 JavaScript 所有的类都是由 Object 派生而来的,所以,我们决定为 Object 新定义一个 $super 方法,该方法可以用于其它所有的类:
Object.prototype.$super = function () {
var result;
try {
// 首先获取基类的构造器,将它保存在变量 result 中
result = eval(this.constructor).prototype.constructor;
// 调用基类的构造器方法
result.apply(this, arguments);
} catch (err) {
// 如果不是内建类或者自定义类,或者不在构造器中调用该方法,那么就抛出错误
throw new Error('only can be used in constructor!');
}
return result;
};
然后就可以使用这个方法了,例如下面的代码:
function Child(myName, myAge) {
//this.$super = Person; // 无需再使用这个语句
this.$super(myName, myAge);
//调用基类构造器
}
// 这对于创建子类很有用, 该子类在执行附加的初始化的同时, 又调用超类构造函数执行超类初始化
// 下面的代码显示了一个完整的应用:
Object.prototype.$super = function () {
var result;
try {
// 首先获取基类的构造器,将它保存在变量 result 当中
result = eval(this.constructor).prototype.constructor;
// 调用基类的构造器方法
result.apply(this, arguments);
} catch (err) {
// 如果不是内建类或者自定义类,或者不在构造器中调用该方法,那么就抛出错误
throw new Error('only can be used in constructor!');
}
return result;
};
function Person() {
if (arguments.length > 2) {
throw new Error('Two Arguments only can be allow!');
}
this.nickName = arguments[0];
this.age = arguments[1];
}
Person.prototype.showInfo = function () {
return '我的名字是 ' + this.nickName + ' ,我现在 ' + this.age + '$1 了。 ';
};
function Child(myName, myAge) {
this.$super(myName, myAge); // 调用基类构造器
}
Child.prototype = new Person();
var child_1 = new Child('Jane', 8);
console.log(child_1.showInfo());
在创建子类时需要注意:
继承是类的一个强大功能,一个类(子类 / 派生类)可以继承另一类(父类 / 基类)的功能。子类将包含有父类的所有属性和方法,并可以加上其它属性和方法。当然,也可以覆写父类的方法和属性。
通过继承关系,可以扩展用户自己的自定义类,也可以扩展任何内建 JavaScript 核心类。
例如以下代码扩展内建的 String 类,新增了 3 个实用方法:
String.prototype.ltrim = function () {
return this.replace(/^(\s*| *)/, '');
};
String.prototype.rtrim = function () {
return this.replace(/(\s*| *)$/, '');
};
String.prototype.trim = function () {
var t = this.rtrim();
return t.ltrim();
};
不过,现在 ECMAScript5 的 String 类新增了 trim 方法,用户可以检测该方法是否存在再做扩展,如果不存在 trim 方法,就会返回 undefined :
if (!String.prototype.trim) {
String.prototype.trim = function () {
var t = this.rtrim();
return t.ltrim();
};
}
例如,可以按如下方法定义 Window 类和 Door 类:
// Window
function Window() {
this.open = function () {
// 这里包含一些语句,用于处理打开窗子的动作
};
this.close = function () {
// 这里包含一些语句,用于处理关闭窗子的动作
};
}
// Door
function Door() {
this.open = function () {
// 这里包含一些语句,用于处理打开门的动作
};
this.close = function () {
// 这里包含一些语句,用于处理关闭门的动作
};
}
然后在 House 类中组合它们,在相应的方法中调用对象的方法:
function House() {
this.openWindow = function () {
var window = new Window();
window.open(); // 开窗操作
};
this.closeWindow = function () {
var window = new Window();
window.close(); // 关窗操作
};
this.openDoor = function () {
var door = new Door();
door.open(); // 开门操作
};
this.closeDoor = function () {
var door = new Door();
door.close();
// 关门操作
};
}
假定不组合 Window 类和 Door 类,那么我们就必须在 House 类的每个方法中定义相应的代码处理相应的操作,而不能重用 Window 类和 Door 类中定义的代码:
function House() {
this.openWindow = function () {
// 这里包含一些语句,用于处理打开窗子的动作
};
this.closeWindow = function () {
// 这里包含一些语句,用于处理关闭窗子的动作
};
this.openDoor = function () {
// 这里包含一些语句,用于处理打开门的动作
};
this.closeDoor = function () {
// 这里包含一些语句,用于处理关闭门的动作
};
}
总体来说,类的组合有以下两个优点:
is-a 表示的是属于的关系,例如兔子属于一种动物,那么兔子就可以继承动物这个类,它们之间是继承关系。再例如 Employee 继承 Person ,它们之间也是继承关系:
Employee is a Person
has-a 表示的是包含关系,例如兔子包含有腿、头等组件,这时就不能说兔子腿是属于一种兔子,它们不是继承关系,而是组合关系。再例如 House 包含 Window 和 Door ,它们之间也是组合关系:
House has a Window and Door
在一些 OOP 开发的文档中,可能会看到 Aggregation (聚合)和 Composition (合成)这两个不同的名词,两者都是表示一个类" 拥有" 另一些类,都表示的是组合关系,但是有些许差别:
聚合表示一种弱的" 拥有" 关系,体现的是 A 对象可以包含 B 对象,但 B 对象不是 A 对象的一部分,例如,存在大雁与雁群这两个类,大雁是群居动物,每只大雁都属于一个雁群,一个雁群可以有多只大雁。所以它们之间就满足聚合关系。
合成是一种强的" 拥有" 关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。例如 House 包含 Window 和 Door ,就是合成关系。
聚合和合成仅仅是两个概念,在实际运用中,组合这个统一的概念已经可以非常清楚地表示两个类之间的关系了,一般都无需做更细微的区分,但一些 UML 类图中还是会做这些细微的区分。