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

TypeScript 类的继承链与多态性实现

2022-05-291.8k 阅读

TypeScript 类的继承链

在 TypeScript 中,类的继承是一项强大的特性,它允许我们基于已有的类创建新的类。通过继承,新类(子类)可以获得其父类的属性和方法,同时还可以添加自己特有的属性和方法。这就形成了一条继承链,从最顶层的基类开始,层层向下传递属性和行为。

继承的基本语法

我们来看一个简单的示例,假设有一个基类 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 类使用 extends 关键字继承了 Animal 类。Dog 类不仅拥有 Animal 类的 name 属性和 speak 方法,还添加了自己的 breed 属性和 bark 方法。在 Dog 类的构造函数中,我们使用 super 关键字调用了父类 Animal 的构造函数,以初始化从父类继承来的 name 属性。

继承链的层级结构

继承链可以有多个层级。例如,我们可以在 Dog 类的基础上再创建一个子类 Poodle

class Poodle extends Dog {
    size: string;
    constructor(name: string, breed: string, size: string) {
        super(name, breed);
        this.size = size;
    }
    performTrick() {
        console.log(`${this.name} performs a trick.`);
    }
}

这里 Poodle 类继承自 Dog 类,而 Dog 类又继承自 Animal 类。所以 Poodle 类拥有 Animal 类和 Dog 类的所有可访问属性和方法,同时还添加了自己的 size 属性和 performTrick 方法。这样就形成了一个三层的继承链:Animal -> Dog -> Poodle

继承链中的访问修饰符

在继承链中,访问修饰符起着重要的作用,它决定了属性和方法在子类中的可访问性。TypeScript 中有三种访问修饰符:publicprivateprotected

  • public:默认的访问修饰符,属性或方法可以在类的内部、子类以及类的实例外部访问。例如,在上述 AnimalDogPoodle 类中,name 属性和 speak 方法都是 public 的,所以在子类和类的实例中都可以访问。
let myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // 可以正常调用,因为speak方法是public的
  • private:标记为 private 的属性或方法只能在类的内部访问,子类和类的实例外部都无法访问。例如,如果我们将 Animal 类中的 name 属性改为 private
class Animal {
    private 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;
    }
    bark() {
        // 这里无法访问父类的private属性name
        // console.log(`${this.name} barks.`); // 报错
    }
}
  • protectedprotected 属性或方法可以在类的内部以及子类中访问,但不能在类的实例外部访问。如果将 Animal 类中的 name 属性改为 protected
class Animal {
    protected 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;
    }
    bark() {
        console.log(`${this.name} barks.`); // 可以访问,因为name属性是protected的
    }
}
let myDog = new Dog('Buddy', 'Golden Retriever');
// myDog.name; // 报错,不能在类的实例外部访问protected属性

多态性的概念

多态性是面向对象编程中的一个重要概念,它允许不同类的对象对同一消息做出不同的响应。简单来说,就是用同一个方法名,根据对象的不同类型执行不同的实现。在 TypeScript 中,多态性主要通过继承和方法重写来实现。

多态性在继承中的体现

结合前面的 AnimalDogPoodle 类的例子,我们来进一步说明多态性。假设我们有一个函数,它接受一个 Animal 类型的参数,并调用其 speak 方法:

function makeSound(animal: Animal) {
    animal.speak();
}
let myAnimal = new Animal('Generic Animal');
let myDog = new Dog('Buddy', 'Golden Retriever');
let myPoodle = new Poodle('Luna', 'Poodle', 'Small');
makeSound(myAnimal);
makeSound(myDog);
makeSound(myPoodle);

在上述代码中,makeSound 函数接受一个 Animal 类型的参数。虽然参数类型都是 Animal,但当传入 myAnimalmyDogmyPoodle 时,会调用各自类中定义的 speak 方法。Animal 类的 speak 方法输出通用的声音描述,Dog 类的 speak 方法可能会输出狗叫的描述,而 Poodle 类的 speak 方法也会输出与贵宾犬相关的声音描述。这就是多态性的体现,同一个方法名 speak,根据对象的实际类型(AnimalDogPoodle)执行不同的实现。

TypeScript 中多态性的实现

方法重写

在 TypeScript 中,子类可以重写从父类继承来的方法,以实现多态性。方法重写是指子类定义一个与父类中同名、同参数列表和同返回类型(或兼容返回类型)的方法。例如,我们在 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() {
        console.log(`${this.name} barks.`);
    }
    bark() {
        console.log(`${this.name} barks.`);
    }
}

在这个例子中,Dog 类重写了 Animal 类的 speak 方法。当我们创建一个 Dog 类的实例并调用 speak 方法时,会执行 Dog 类中重写后的 speak 方法,而不是 Animal 类中的原始方法。

let myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // 输出: Buddy barks.

抽象类和抽象方法

抽象类是一种不能被实例化的类,它主要用于为子类提供一个通用的基类,其中可以包含抽象方法。抽象方法是只有声明而没有实现的方法,必须在子类中被重写。这也是实现多态性的一种重要方式。

abstract class Shape {
    abstract getArea(): number;
}
class Circle extends Shape {
    radius: number;
    constructor(radius: number) {
        super();
        this.radius = radius;
    }
    getArea(): number {
        return Math.PI * this.radius * this.radius;
    }
}
class Rectangle extends Shape {
    width: number;
    height: number;
    constructor(width: number, height: number) {
        super();
        this.width = width;
        this.height = height;
    }
    getArea(): number {
        return this.width * this.height;
    }
}
function printArea(shape: Shape) {
    console.log(`The area of the shape is ${shape.getArea()}`);
}
let circle = new Circle(5);
let rectangle = new Rectangle(4, 5);
printArea(circle);
printArea(rectangle);

在上述代码中,Shape 是一个抽象类,它包含一个抽象方法 getAreaCircleRectangle 类继承自 Shape 类,并分别重写了 getArea 方法以计算各自的面积。printArea 函数接受一个 Shape 类型的参数,通过多态性,它可以调用不同子类中重写的 getArea 方法来计算并输出不同形状的面积。

多态性与接口

在 TypeScript 中,接口也可以用于实现多态性。接口定义了一组方法的签名,但不包含方法的实现。类可以实现一个或多个接口,通过实现相同接口的不同类,我们可以在这些类的实例上实现多态性。

interface Drawable {
    draw(): void;
}
class Square implements Drawable {
    side: number;
    constructor(side: number) {
        this.side = side;
    }
    draw(): void {
        console.log(`Drawing a square with side ${this.side}`);
    }
}
class Triangle implements Drawable {
    base: number;
    height: number;
    constructor(base: number, height: number) {
        this.base = base;
        this.height = height;
    }
    draw(): void {
        console.log(`Drawing a triangle with base ${this.base} and height ${this.height}`);
    }
}
function drawShape(shape: Drawable) {
    shape.draw();
}
let square = new Square(4);
let triangle = new Triangle(3, 4);
drawShape(square);
drawShape(triangle);

在这个例子中,Drawable 是一个接口,定义了 draw 方法。SquareTriangle 类都实现了 Drawable 接口,并各自实现了 draw 方法。drawShape 函数接受一个 Drawable 类型的参数,通过多态性,可以调用不同类中实现的 draw 方法来绘制不同的形状。

继承链与多态性的结合应用

在实际的前端开发中,继承链与多态性常常结合使用,以提高代码的可维护性和扩展性。例如,在一个图形绘制库中,我们可能有一个基类 Graphic,它定义了一些通用的属性和方法,如位置、颜色等。然后有子类 RectangleGraphicCircleGraphic 等继承自 Graphic 类,并且重写了绘制方法以实现各自的图形绘制逻辑。

class Graphic {
    x: number;
    y: number;
    color: string;
    constructor(x: number, y: number, color: string) {
        this.x = x;
        this.y = y;
        this.color = color;
    }
    draw() {
        console.log(`Drawing a graphic at (${this.x}, ${this.y}) with color ${this.color}`);
    }
}
class RectangleGraphic extends Graphic {
    width: number;
    height: number;
    constructor(x: number, y: number, color: string, width: number, height: number) {
        super(x, y, color);
        this.width = width;
        this.height = height;
    }
    draw() {
        console.log(`Drawing a rectangle at (${this.x}, ${this.y}) with width ${this.width}, height ${this.height} and color ${this.color}`);
    }
}
class CircleGraphic extends Graphic {
    radius: number;
    constructor(x: number, y: number, color: string, radius: number) {
        super(x, y, color);
        this.radius = radius;
    }
    draw() {
        console.log(`Drawing a circle at (${this.x}, ${this.y}) with radius ${this.radius} and color ${this.color}`);
    }
}
let graphics: Graphic[] = [];
graphics.push(new RectangleGraphic(10, 10, 'red', 50, 30));
graphics.push(new CircleGraphic(50, 50, 'blue', 20));
graphics.forEach((graphic) => {
    graphic.draw();
});

在上述代码中,我们创建了一个 Graphic 类的继承链,RectangleGraphicCircleGraphic 类继承自 Graphic 类并各自重写了 draw 方法。通过将不同类型的图形对象放入一个数组,并遍历调用 draw 方法,实现了多态性。这样,当我们需要添加新的图形类型时,只需要创建一个新的子类继承自 Graphic 类,并实现 draw 方法即可,而不需要修改现有的遍历和绘制逻辑,大大提高了代码的扩展性。

继承链与多态性的注意事项

避免过度继承

虽然继承是一种强大的特性,但过度使用继承可能会导致代码的复杂性增加。例如,如果继承链过长,维护和理解代码会变得困难。当一个基类发生变化时,可能会影响到整个继承链上的所有子类。因此,在设计类的继承结构时,要谨慎考虑,尽量保持继承链的简洁。可以考虑使用组合(将一个类作为另一个类的属性)等其他设计模式来替代复杂的继承关系。

方法重写的兼容性

在重写方法时,要确保子类中重写的方法与父类中被重写的方法具有相同的参数列表和兼容的返回类型。如果参数列表或返回类型不兼容,TypeScript 编译器会报错。例如,在重写 Animal 类的 speak 方法时,如果 Dog 类的 speak 方法参数列表或返回类型与 Animal 类的 speak 方法不一致,就会出现编译错误。

抽象类和接口的选择

在决定使用抽象类还是接口来实现多态性时,需要考虑具体的需求。抽象类可以包含属性和方法的实现,适合用于创建具有一些共同行为和状态的基类。而接口只定义方法签名,不包含实现,更适合用于定义一组相关的行为,多个不相关的类可以实现同一个接口以获得多态性。如果需要为子类提供一些通用的实现代码,抽象类是更好的选择;如果只是希望不同类具有相同的方法签名,接口更合适。

总结(此部分为辅助理解,实际写作不包含)

通过以上内容,我们深入了解了 TypeScript 中类的继承链与多态性的实现。继承链允许我们构建层次化的类结构,通过继承传递属性和方法。多态性则通过方法重写、抽象类和接口等方式,使不同类的对象对同一消息做出不同的响应。在实际开发中,合理运用继承链与多态性可以提高代码的复用性、可维护性和扩展性,是前端开发中非常重要的技术手段。但同时也要注意避免过度继承等问题,以确保代码的质量和可读性。在前端开发的各种场景,如构建 UI 组件库、游戏开发等方面,继承链与多态性都有着广泛的应用,能够帮助我们更高效地实现复杂的功能。