MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript类继承与多态的实现

2021-04-117.6k 阅读

类继承基础概念

在 TypeScript 中,类继承是一种机制,它允许一个类获取另一个类的属性和方法。被继承的类称为父类(或基类),继承的类称为子类(或派生类)。通过继承,子类可以复用父类的代码,同时还能添加自己特有的属性和方法,这极大地提高了代码的可维护性和可扩展性。

在 TypeScript 中,使用 extends 关键字来实现类继承。例如,假设有一个 Animal 类作为父类:

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}

现在我们可以创建一个 Dog 类继承自 Animal 类:

class Dog extends Animal {
    breed: string;
    constructor(name: string, breed: string) {
        super(name);
        this.breed = breed;
    }
    bark() {
        console.log(`${this.name} barks.`);
    }
}

在上述代码中,Dog 类继承了 Animal 类的 name 属性和 speak 方法。在 Dog 类的构造函数中,通过 super(name) 调用了父类 Animal 的构造函数,以便正确初始化从父类继承的 name 属性。同时,Dog 类添加了自己特有的 breed 属性和 bark 方法。

继承中的属性和方法访问控制

TypeScript 提供了三种访问修饰符来控制类中属性和方法的访问权限:publicprivateprotected

  1. public(公共的):这是默认的访问修饰符。被 public 修饰的属性和方法可以在类的内部、子类以及类的实例外部访问。例如,在上面的 AnimalDog 类中,name 属性和 speak 方法都是 public 的,所以在 Dog 类内部以及创建 Dog 实例后都可以访问它们。
let myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // 输出: Buddy makes a sound.
console.log(myDog.name); // 输出: Buddy
  1. private(私有的):被 private 修饰的属性和方法只能在类的内部访问,子类和类的实例外部都无法访问。例如,我们可以在 Animal 类中添加一个私有属性 private age: number;,然后在 Animal 类的构造函数中初始化它 this.age = age;。如果在 Dog 类或 Animal 实例外部尝试访问 age 属性,TypeScript 会报错。
class Animal {
    name: string;
    private age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}
class Dog extends Animal {
    breed: string;
    constructor(name: string, age: number, breed: string) {
        super(name, age);
        this.breed = breed;
    }
    bark() {
        // 这里尝试访问父类的私有属性 age 会报错
        // console.log(`${this.name} is ${this.age} years old and barks.`);
        console.log(`${this.name} barks.`);
    }
}
let myDog = new Dog('Buddy', 3, 'Golden Retriever');
// 这里尝试访问私有属性 age 会报错
// console.log(myDog.age);
  1. protected(受保护的):被 protected 修饰的属性和方法可以在类的内部以及子类中访问,但不能在类的实例外部访问。例如,我们将 Animal 类中的 age 属性改为 protected
class Animal {
    name: string;
    protected age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}
class Dog extends Animal {
    breed: string;
    constructor(name: string, age: number, breed: string) {
        super(name, age);
        this.breed = breed;
    }
    bark() {
        console.log(`${this.name} is ${this.age} years old and barks.`);
    }
}
let myDog = new Dog('Buddy', 3, 'Golden Retriever');
// 这里尝试访问受保护属性 age 会报错
// console.log(myDog.age);

Dog 类中,我们可以访问从父类 Animal 继承的 protected 属性 age,但在 Dog 实例外部不能访问。

方法重写

在类继承中,子类可以重写父类的方法,以提供更符合自身需求的实现。当子类重写父类方法时,方法的签名(参数列表和返回类型)必须与父类方法相同。继续以上面的 AnimalDog 类为例,假设我们想让 Dog 类的 speak 方法有不同的实现:

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}
class Dog extends Animal {
    breed: string;
    constructor(name: string, breed: string) {
        super(name);
        this.breed = breed;
    }
    // 重写父类的 speak 方法
    speak() {
        console.log(`${this.name} barks.`);
    }
    bark() {
        console.log(`${this.name} barks.`);
    }
}
let myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // 输出: Buddy barks.

在上述代码中,Dog 类重写了 Animal 类的 speak 方法。当调用 myDog.speak() 时,执行的是 Dog 类中重写后的 speak 方法。

调用父类方法

在子类重写方法中,有时需要调用父类被重写的方法,以复用部分逻辑。可以使用 super 关键字来调用父类的方法。例如,假设我们在 Dog 类的 speak 方法中,除了输出狗叫的信息,还想先输出父类 Animalspeak 方法的信息:

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}
class Dog extends Animal {
    breed: string;
    constructor(name: string, breed: string) {
        super(name);
        this.breed = breed;
    }
    // 重写父类的 speak 方法
    speak() {
        super.speak();
        console.log(`${this.name} barks.`);
    }
    bark() {
        console.log(`${this.name} barks.`);
    }
}
let myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); 
// 输出: 
// Buddy makes a sound.
// Buddy barks.

Dog 类的 speak 方法中,通过 super.speak() 调用了父类 Animalspeak 方法,然后再输出狗叫的信息。

多态的概念与实现

多态是面向对象编程的重要特性之一,它允许不同类的对象对同一消息做出不同的响应。在 TypeScript 中,多态通过类继承和方法重写来实现。

基于继承和方法重写的多态

假设有一个父类 Shape,它有一个 draw 方法用于绘制图形,但具体的绘制逻辑在子类中实现。然后有两个子类 CircleRectangle,它们继承自 Shape 类并分别重写 draw 方法以实现各自的绘制逻辑。

class Shape {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    draw() {
        console.log(`${this.name} is drawn.`);
    }
}
class Circle extends Shape {
    radius: number;
    constructor(name: string, radius: number) {
        super(name);
        this.radius = radius;
    }
    draw() {
        console.log(`${this.name} (Circle with radius ${this.radius}) is drawn.`);
    }
}
class Rectangle extends Shape {
    width: number;
    height: number;
    constructor(name: string, width: number, height: number) {
        super(name);
        this.width = width;
        this.height = height;
    }
    draw() {
        console.log(`${this.name} (Rectangle with width ${this.width} and height ${this.height}) is drawn.`);
    }
}
// 创建不同形状的对象数组
let shapes: Shape[] = [
    new Circle('Circle1', 5),
    new Rectangle('Rectangle1', 10, 5)
];
// 遍历数组并调用 draw 方法,根据对象实际类型执行不同的 draw 逻辑
for (let shape of shapes) {
    shape.draw();
}
// 输出:
// Circle1 (Circle with radius 5) is drawn.
// Rectangle1 (Rectangle with width 10 and height 5) is drawn.

在上述代码中,Shape 类是所有形状类的基类,它定义了一个通用的 draw 方法。CircleRectangle 类继承自 Shape 类并各自重写了 draw 方法。当我们创建一个 Shape 类型的数组,并将 CircleRectangle 对象放入其中,然后遍历这个数组并调用 draw 方法时,实际执行的是每个对象对应的 draw 方法,这就是多态的体现。

接口与多态

接口也可以用于实现多态。接口定义了一组方法的签名,但不包含方法的实现。多个类可以实现同一个接口,并根据自身需求实现接口中的方法,从而实现多态。

例如,定义一个 Drawable 接口,然后让 CircleRectangle 类实现这个接口:

interface Drawable {
    draw(): void;
}
class Circle implements Drawable {
    name: string;
    radius: number;
    constructor(name: string, radius: number) {
        this.name = name;
        this.radius = radius;
    }
    draw() {
        console.log(`${this.name} (Circle with radius ${this.radius}) is drawn.`);
    }
}
class Rectangle implements Drawable {
    name: string;
    width: number;
    height: number;
    constructor(name: string, width: number, height: number) {
        this.name = name;
        this.width = width;
        this.height = height;
    }
    draw() {
        console.log(`${this.name} (Rectangle with width ${this.width} and height ${this.height}) is drawn.`);
    }
}
// 创建不同形状的对象数组
let drawables: Drawable[] = [
    new Circle('Circle1', 5),
    new Rectangle('Rectangle1', 10, 5)
];
// 遍历数组并调用 draw 方法,根据对象实际类型执行不同的 draw 逻辑
for (let drawable of drawables) {
    drawable.draw();
}
// 输出:
// Circle1 (Circle with radius 5) is drawn.
// Rectangle1 (Rectangle with width 10 and height 5) is drawn.

在这个例子中,CircleRectangle 类都实现了 Drawable 接口,并且实现了接口中的 draw 方法。通过将 CircleRectangle 对象放入 Drawable 类型的数组中,并遍历调用 draw 方法,实现了多态效果。

抽象类与抽象方法在多态中的应用

抽象类

抽象类是一种不能被实例化的类,它主要用于作为其他类的基类,为子类提供一个通用的接口。在 TypeScript 中,使用 abstract 关键字来定义抽象类。例如,我们可以将前面的 Shape 类改为抽象类:

abstract class Shape {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    // 抽象方法,子类必须实现
    abstract draw(): void;
}
class Circle extends Shape {
    radius: number;
    constructor(name: string, radius: number) {
        super(name);
        this.radius = radius;
    }
    draw() {
        console.log(`${this.name} (Circle with radius ${this.radius}) is drawn.`);
    }
}
class Rectangle extends Shape {
    width: number;
    height: number;
    constructor(name: string, width: number, height: number) {
        super(name);
        this.width = width;
        this.height = height;
    }
    draw() {
        console.log(`${this.name} (Rectangle with width ${this.width} and height ${this.height}) is drawn.`);
    }
}
// 尝试实例化抽象类会报错
// let shape = new Shape('Generic Shape'); 
// 创建不同形状的对象数组
let shapes: Shape[] = [
    new Circle('Circle1', 5),
    new Rectangle('Rectangle1', 10, 5)
];
// 遍历数组并调用 draw 方法,根据对象实际类型执行不同的 draw 逻辑
for (let shape of shapes) {
    shape.draw();
}
// 输出:
// Circle1 (Circle with radius 5) is drawn.
// Rectangle1 (Rectangle with width 10 and height 5) is drawn.

在上述代码中,Shape 类被定义为抽象类,并且包含一个抽象方法 draw。抽象方法只有方法签名,没有具体的实现,子类必须实现这个抽象方法。由于 Shape 是抽象类,不能直接实例化。CircleRectangle 类继承自 Shape 类,并实现了 draw 方法,从而实现了多态。

抽象方法

抽象方法是抽象类中定义的没有实现的方法,它强制子类必须实现该方法。抽象方法的存在使得抽象类成为一种规范,子类必须按照这个规范来实现具体的行为。在上面的例子中,Shape 类的 draw 方法就是抽象方法。通过这种方式,我们可以确保所有继承自 Shape 类的子类都有一个 draw 方法的具体实现,这在实现多态时非常有用。例如,如果我们有一个新的子类 Triangle 继承自 Shape 类:

abstract class Shape {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    // 抽象方法,子类必须实现
    abstract draw(): void;
}
class Circle extends Shape {
    radius: number;
    constructor(name: string, radius: number) {
        super(name);
        this.radius = radius;
    }
    draw() {
        console.log(`${this.name} (Circle with radius ${this.radius}) is drawn.`);
    }
}
class Rectangle extends Shape {
    width: number;
    height: number;
    constructor(name: string, width: number, height: number) {
        super(name);
        this.width = width;
        this.height = height;
    }
    draw() {
        console.log(`${this.name} (Rectangle with width ${this.width} and height ${this.height}) is drawn.`);
    }
}
class Triangle extends Shape {
    side1: number;
    side2: number;
    side3: number;
    constructor(name: string, side1: number, side2: number, side3: number) {
        super(name);
        this.side1 = side1;
        this.side2 = side2;
        this.side3 = side3;
    }
    draw() {
        console.log(`${this.name} (Triangle with sides ${this.side1}, ${this.side2}, ${this.side3}) is drawn.`);
    }
}
// 创建不同形状的对象数组
let shapes: Shape[] = [
    new Circle('Circle1', 5),
    new Rectangle('Rectangle1', 10, 5),
    new Triangle('Triangle1', 3, 4, 5)
];
// 遍历数组并调用 draw 方法,根据对象实际类型执行不同的 draw 逻辑
for (let shape of shapes) {
    shape.draw();
}
// 输出:
// Circle1 (Circle with radius 5) is drawn.
// Rectangle1 (Rectangle with width 10 and height 5) is drawn.
// Triangle1 (Triangle with sides 3, 4, 5) is drawn.

Triangle 类继承自 Shape 类,并实现了 draw 方法,以符合抽象类 Shape 的规范。这样,Triangle 对象也可以加入到 Shape 类型的数组中,通过多态调用 draw 方法,实现不同的绘制逻辑。

多重继承与混入(Mixin)

多重继承的问题与限制

在一些编程语言中,支持多重继承,即一个类可以从多个父类继承属性和方法。然而,多重继承会带来一些问题,比如菱形继承问题(也称为钻石问题)。假设我们有四个类:ABCD,其中 BC 都继承自 A,而 D 又同时继承自 BC。如果 A 类有一个方法 methodBC 可能会对这个方法有不同的实现,那么 D 类在调用 method 方法时就会出现歧义,不知道应该调用 B 还是 C 中的实现。

在 TypeScript 中,不支持传统的多重继承,因为它会带来复杂性和潜在的冲突。为了解决类似多重继承的需求,TypeScript 提供了其他的机制,如混入(Mixin)。

混入(Mixin)的实现

混入是一种将多个对象的功能合并到一个对象中的技术。在 TypeScript 中,可以通过函数和类来实现混入。下面是一个简单的混入示例:

// 定义一个混入函数
function mixin(target: any, ...sources: any[]) {
    sources.forEach(source => {
        Object.getOwnPropertyNames(source.prototype).forEach(name => {
            target.prototype[name] = source.prototype[name];
        });
    });
    return target;
}
// 定义一个基础类
class Base {
    baseMethod() {
        console.log('This is a base method.');
    }
}
// 定义第一个混入类
class Mixin1 {
    mixin1Method() {
        console.log('This is a method from Mixin1.');
    }
}
// 定义第二个混入类
class Mixin2 {
    mixin2Method() {
        console.log('This is a method from Mixin2.');
    }
}
// 使用混入函数创建一个新类
class MyClass extends Base {}
mixin(MyClass, Mixin1, Mixin2);
let myObject = new MyClass();
myObject.baseMethod();
myObject.mixin1Method();
myObject.mixin2Method();
// 输出:
// This is a base method.
// This is a method from Mixin1.
// This is a method from Mixin2.

在上述代码中,我们定义了一个 mixin 函数,它接受一个目标类 target 和多个源类 sources。通过遍历源类的原型属性,将其复制到目标类的原型上,从而实现将多个类的功能混入到一个类中。这里 MyClass 类继承自 Base 类,然后通过 mixin 函数混入了 Mixin1Mixin2 类的方法,使得 MyClass 实例可以调用 BaseMixin1Mixin2 类的方法。

类继承与多态在实际项目中的应用场景

代码复用与分层架构

在大型前端项目中,类继承可以极大地提高代码复用率。例如,在一个基于 React 的项目中,可能有多个组件需要一些共同的属性和方法,如数据获取、状态管理等。我们可以创建一个基类,将这些共同的逻辑封装在基类中,然后让具体的组件类继承自这个基类。这样,当需要修改这些共同逻辑时,只需要在基类中修改一次,所有继承自它的子类都会自动应用这些修改。

在分层架构中,类继承也有重要应用。比如在一个典型的三层架构(表现层、业务逻辑层、数据访问层)中,业务逻辑层的一些基础业务类可以作为父类,具体的业务子类继承自这些基础业务类,并根据不同的业务需求进行扩展和重写。例如,在一个电商项目中,可能有一个基础的 ProductService 类,包含一些通用的产品操作方法,如获取产品列表、获取单个产品等。然后有 ElectronicsProductServiceClothingProductService 等子类,继承自 ProductService 类,并根据电子产品和服装产品的特点重写或扩展一些方法。

插件系统与扩展性

多态在实现插件系统时非常有用。假设我们正在开发一个图形绘制库,希望支持不同类型的图形绘制插件。我们可以定义一个抽象类 GraphicPlugin,其中包含一些抽象方法,如 init(初始化插件)、draw(绘制图形)等。然后每个具体的插件类,如 CirclePluginRectanglePlugin 等继承自 GraphicPlugin 类,并实现这些抽象方法。在主程序中,通过维护一个 GraphicPlugin 类型的数组来管理所有插件。当需要绘制图形时,遍历这个数组,调用每个插件的 draw 方法,根据插件的实际类型执行不同的绘制逻辑,从而实现插件系统的扩展性。

事件处理与回调机制

在前端开发中,事件处理和回调机制经常用到类继承和多态。例如,在一个游戏开发项目中,可能有多种类型的游戏对象,如玩家、敌人、道具等。每个游戏对象都可能需要处理碰撞事件。我们可以创建一个基类 GameObject,其中定义一个 handleCollision 方法用于处理碰撞事件。然后不同类型的游戏对象类,如 PlayerEnemyItem 等继承自 GameObject 类,并根据自身特点重写 handleCollision 方法。当发生碰撞事件时,通过多态调用不同游戏对象的 handleCollision 方法,实现不同的碰撞处理逻辑。

总结类继承与多态在前端开发中的优势

  1. 代码复用:通过类继承,子类可以复用父类的代码,减少重复代码的编写,提高开发效率。这在大型项目中尤为重要,避免了在多个地方重复实现相同的功能。
  2. 可维护性:当需要修改共同的逻辑时,只需要在父类中进行修改,所有子类都会自动应用这些修改。这使得代码的维护更加容易,降低了维护成本。
  3. 可扩展性:多态允许在不修改现有代码的情况下,轻松添加新的类型和行为。例如,在插件系统中,可以随时添加新的插件类,只要它们遵循基类或接口的规范,就可以无缝集成到系统中。
  4. 清晰的代码结构:类继承和多态有助于创建清晰的代码结构,将相关的功能组织在一起,提高代码的可读性和可理解性。例如,在分层架构中,通过继承和多态可以清晰地划分不同层次的职责。

在前端开发中,TypeScript 的类继承与多态是强大的工具,合理运用它们可以提高代码质量、开发效率和项目的可维护性与可扩展性。无论是小型项目还是大型企业级应用,理解和掌握这些概念对于前端开发者来说都是至关重要的。