一、介绍
1. 继承定义
继承:子类继承父类,子类拥有父类的所有特性,并且可以有自己一些更具体的特性。
2. 接口继承和实现继承
许多编程语言支持两种继承方式:接口继承(interface inheritance)和实现继承(implementation inheritance)。
- 接口继承(Interface Inheritance):
在接口继承中,一个类
可以从一个或多个接口
继承方法声明,但不继承实现。接口定义了类应该具有的方法和属性,但不提供具体的实现。
实现类
必须提供接口中声明的所有方法的具体实现。
接口继承的优势在于它提供了一种规范或契约,允许多个类共享相同的接口。
通常情况下,一个类可以实现多个接口,从而实现多态性和组合性。
多态:由继承产生了相关的不同类,对同一个方法可以有不同的响应,即继承同一个父类,但子类个自己实现自己的方法。
- 实现继承(Implementation Inheritance):
在实现继承中,一个类
可以从另一个类
继承方法和属性的实现。
子类(或派生类)
继承父类(或基类)
的行为和属性,并且可以通过重写方法来修改或扩展父类的行为。
实现继承通常通过类的继承关系来实现,其中子类继承父类的方法和属性,并且可以添加新的方法和属性。
一些编程语言支持接口继承和实现继承的结合使用,例如Java。在Java中,一个类可以继承另一个类(实现继承),同时实现一个或多个接口(接口继承),从而同时享受到接口的规范性和基类的行为实现。
对于 JavaScript 来说,JavaScript高级程序设计(第4版)中提过一句话,“在 ECMAScript 中,无法实现接口继承,因为函数没有签名”。在JavaScript中函数的参数数量、类型和返回值类型都不是函数类型的一部分。因此,无法通过函数的签名(参数和返回值类型)来定义接口,也就无法通过函数的签名来实现接口继承。
举个例子,假设我们想要定义一个接口Drawable,要求实现类必须具有一个draw方法,以及一个接口Resizable,要求实现类必须具有一个resize方法。在其他语言中,我们可以像下面这样定义:
interface Drawable {
void draw();
}
interface Resizable {
void resize(int width, int height);
}
class Rectangle implements Drawable, Resizable {
// 实现 draw 方法
public void draw() {
// 绘制矩形
}
// 实现 resize 方法
public void resize(int width, int height) {
// 调整矩形大小
}
}
但在JavaScript中,我们无法直接定义接口,并要求类去实现它们。JavaScript是一种动态类型语言,函数的签名(参数和返回值类型)在定义函数时并不是必需的,因此无法利用函数签名来实现接口继承。相反,在JavaScript中,我们通常通过文档或约定来定义对象应该具有的方法或属性,然后在实现类中按照这些约定来编写代码。
当然,现在流行使用 Typescript,我们可以使用接口继承来实现类似于其他语言中的接口继承的概念。TypeScript 的接口(interfaces)可以被一个类实现,从而约束该类必须具有接口中定义的属性和方法。这种方式类似于其他语言中的接口继承。
// 定义一个接口
interface Animal {
name: string;
makeSound(): string;
}
// 定义一个实现了 Animal 接口的类
class Dog implements Animal {
constructor(public name: string) {}
makeSound() {
return "Dog barks";
}
}
// 定义一个实现了 Animal 接口的类
class Cat implements Animal {
constructor(public name: string) {}
makeSound() {
return "Cat meows";
}
}
// 使用实现了 Animal 接口的类
const dog = new Dog("Buddy");
console.log(dog.name); // 输出: Buddy
console.log(dog.makeSound()); // 输出: Dog barks
// 使用实现了 Animal 接口的类
const cat = new Cat("Whiskers");
console.log(cat.name); // 输出: Whiskers
console.log(cat.makeSound()); // 输出: Cat meows
只不过,Typescript 本质上也属于类型约定。
3. 继承实现的方式
关于 JavaScript 继承实现一般有四种主要的继承方法:
- 基于原型链的继承
- 基于构造函数的继承
- 组合式继承
- class extends 继承
关于第 1、2 种的具体实现,可以自行学习。这里阐述一下他们存在的问题。
基于原型链的继承(Prototype-based Inheritance)和基于构造函数的继承(Constructor-based Inheritance)都是 JavaScript 中常见的继承方式,但它们各自存在一些问题:
基于原型链的继承:
-
共享属性和方法: 在原型链继承中,子类实例共享父类原型上的属性和方法。如果一个实例修改了原型上的属性或方法,其他实例也会受到影响,这可能会导致意外的副作用。
-
无法向父类传递参数: 在原型链继承中,子类无法向父类构造函数传递参数,因为子类实例是通过原型链链接到父类构造函数的,无法直接访问父类构造函数。
基于构造函数的继承:
-
无法继承父类原型上的属性和方法: 在构造函数继承中,子类无法继承父类原型上的属性和方法,因为它们之间没有原型链的关系。
-
方法重复定义: 如果父类的方法在构造函数内定义,那么每个子类实例都会创建一个新的方法实例,这会导致内存浪费。
因此,通常在实际开发中,会结合使用两种继承方式来克服各自的局限性,或者使用其他模式(如组合继承、工厂模式等)来实现继承,包括现代 Class 继承。
二、组合式继承实现
1. 介绍
原型链和构造函数组合式继承是JavaScript中常用且灵活的继承方式之一,也称为经典继承模式(Classical Inheritance Pattern)或伪经典继承模式(Pseudo-classical Inheritance Pattern)。
其允许子类继承父类的属性和方法,这种方式结合了原型链继承和构造函数继承的优点,避免了它们各自的缺点。
2. 实现
首先,我们定义一个父类 Animal,其中包含一个构造函数和一些方法:
function Animal(name) {
this.name = name;
}
Animal.prototype.getName = function() {
return this.name;
}
Animal.prototype.makeSound = function() {
return "aooo~";
}
然后,我们定义一个子类 Dog,使用构造函数
继承父类的属性,并通过设置原型链
来继承父类的方法:
function Dog(name, breed) {
Animal.call(this, name); // 调用父类构造函数,继承父类的属性
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype); // 设置子类的原型为父类的实例,继承父类的方法
// Object.create
// function create(prototypeObj) {
// return { '__proto__': prototypeObj };
// }
Dog.prototype.constructor = Dog; // 修正子类的构造函数指向自身
// 子类可以覆盖父类的方法
Dog.prototype.makeSound = function() {
return "Dog barks";
};
3. 缺点
- 重复调用父类构造函数: 在组合继承中,子类在调用父类构造函数时会创建多余的属性副本,导致内存浪费。
- 构造函数中重复定义方法: 如果父类的方法在构造函数中定义,那么每个子类实例都会创建一个新的方法实例,这会导致内存浪费。
可以发现,主要还是在 Parent.call(this)
时会存在一些问题。
虽然组合继承不是完美的继承方式,但它在大多数情况下能够很好地满足需求,尤其是在需要兼顾灵活性和性能的场景下。然而,在一些对性能要求较高的场景下,可能需要考虑其他更优化的继承方式,比如 ES6 中的 class 语法。