继承
继承是面向对象编程核心概念之一,可以帮助我们避免代码重复。主要的思想是我们可以创建一个包含逻辑的基类,可以被子类重用。
- 特性:
JavaScript
类继承使用extends
关键字。- 继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。
super()
方法用于调用父类的构造函数。- 当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类(父类),新建的类称为派生类(子类)。
- 示例:
我们创建了一个基类 “// 基类 class Animal { eat() {} sleep() {} }; //派生类 class Dog extends Animal {}; class Cat extends Animal {};
Animal
”,子类会继承Animal
中的通用逻辑。
继承存在is-a
关系:Dog
是一个Animal
,Cat
也是一个Animal
。
组合
和继承不同,组合使用的是 has-a
关系,将不同的关系收集到一起。
class Car {
constructor(engine, transmission) {
this.engine = engine;
this.transmission = transmission;
}
}
class Engine {
constructor(type) {
this.type = type;
}
}
class Transmission {
constructor(type) {
this.type = type;
}
}
const petrolEngine = new Engine('petrol');
const automaticTransmission = new Engine('automatic');
const passengerCar = new Car(petrolEngine, automaticTransmission) ;
我们创建了使用 Engine
和 Transmission
创建了 Car
,我们不能说 Engine
是一个 Car
,但是可以说 Car
包含 Engine
。
区别
我们再来看两个不同的示例,对比一下使用类的方法实现继承和函数方法实现组合有什么区别。
假设我们正在使用文件系统,想实现读取、写入和删除的功能。我们可以创建一个类:
class FileService {
constructor(filename) {
this.filename = filename;
}
read() {}
write() {}
remove() {}
}
目前可以满足我们想要的功能,之后我们可能想加入权限控制,一些用户只有读取权限,其他人可能有写入权限。我们应该怎么办?一个解决方案是我们可以把方法划分为不同的类:
class FileService {
constructor(filename) {
this.filename = filename;
}
}
class FileReader extends FileService {
read() {}
}
class FileWriter extends FileService {
write() {}
}
class FileRemover extends FileService {
remove() {}
}
目前可以满足我们的需求,但是如果有一些人既有读取又有写入的权限呢?或是只有读取和删除的权限呢?使用当前的实现,我们做不到,应该怎么解决?
我们可能会想到为读取和写入创建一个类,为读取和删除创建一个类。
class FileReaderAndWriter extends FileService {
read() {}
write() {}
}
class FileReaderAndRemover extends FileService {
read() {}
remove() {}
}
但按照这种做法,我们可能还需要以下类: FileReader
, FileWriter
, FileRemove
, FileWriterAndRemove
, FileReaderAndRemove
。
这不是一个好的实现方式:第一,我们可能不仅有 3
种,而是 10
、20
种方法,还需要在他们之间有大量的组合。第二是我们的类中存在重复的逻辑,FileReader
类包含读取方法,FileReaderAndWriter
也包含同样的代码。
这不是一个很好的解决方案,还有其他的实现方法吗?多重继承?JavaScript
中没有这个特性,而且也不是很好的方案:A
类继承了 B
类,B
类可能继承了其他类…,这样的设计会非常混乱,不是一个良好的代码架构。
怎么解决呢?一个合理的方法是使用组合:我们把方法拆分为单独的函数工厂。然后根据需求使用它们:
class FileService {
constructor(filename) {
this.filename = filename ;
}
}
function createReadFileService (filename ) {
const file = new FileService(filename);
return {
...file,
read()
}
}
function createWriteFileService (filename) {
const file = new FileService(filename);
return {
...file,
writer(),
}
}
上面的例子中,我们分别创建了读取和写入服务,如果我们想组合两种权限:读取和写入,我们可以很容易的做到:
function createReadAndWriteFileService (filename) {
const file = new FileService(filename);
return {
...file,
read(),
writer()
}
}
const fileForReadAndWriter = createReadAndWriteFileService('test');
fileForReadAndWriter.read();
fileForReadAndWriter.write();
如果我们有 5
、10
、20
种方法,我们可以按照我们想要的方式进行组合,不会有重复的代码问题,也没有令人困惑的代码架构。
我们再来看一个使用函数的例子,假设我们有很多员工,有出租车司机、健身教练和管理员。
function createDriver(name) {
return {
name,
age,
canDrive: true,
}
}
function createManager(name) {
return {
name,
age,
canManage: true
}
}
function createSportCoach(name) {
return {
name,
age,
canSport: true
}
}
看起来没有问题,但是假设有一些员工白天当健身教练,晚上去跑出租,或者可能有多种类型混合的情况,我们应该怎么办呢?就像第一个案例一样,使用组合是一种不错的方案:
function createEmployee(name,age) {
return {
name,
age
}
}
function createDriver() {
return {
canDrive: true
}
}
function createManager() {
return {
canManage: true
}
}
function createSportCoach() {
return {
canSport: true
}
}
现在我们可以根据需要组合所有工作类型,没有重复代码,也更容易理解:
const driver = {
...createEmployee('Amy', 20),
...createDriver()
}
const manager = {
...createEmployee('Max', 25),
...createManager()
}
const sportCoach = {
...createEmployee('Bob', 23),
...createSportCoach()
}
const sportCoachAndDriver = {
...createEmployee('Tom', 27) ,
...createDriver(),
...createSportCoach()
}
总结
一般来说,继承可以用于 is-a
关系,组合可以用于has-a
。
但继承有时候并不是一个好的解决方法:就像示例中,司机是员工(is-a
关系),经理也是员工,如果我们需要把不同的部分进行混合,组合确实比继承更合适。
继承和组合都是很好实现,我们应该正确的使用他们。一些场景组合可能更合适,反之亦然。
实际上,有时将继承和组合结合在一起是一个不错的选择,比如我们有 is-a
关系,但想添加不同的值或方法:我们可以创建一些基类,为实例提供所有通用功能,然后使用组合来添加其他特定功能。