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

深入理解Typescript的类和继承

2021-06-035.2k 阅读

类的基本概念

在TypeScript中,类是一种面向对象编程的核心概念,它用于封装数据和相关的行为。类就像是一个模板,定义了一组对象的共同特征和行为。通过类,我们可以创建多个具有相同结构和行为的对象实例。

定义类

定义一个简单的类非常直观。以下是一个定义Person类的示例:

class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet() {
        console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
    }
}

在上述代码中,我们定义了一个Person类。类中包含两个属性nameage,它们分别表示人的姓名和年龄,类型分别为stringnumberconstructor是类的构造函数,用于在创建对象实例时初始化属性。greet方法则用于输出问候语,展示对象的属性信息。

创建对象实例

定义好类之后,我们可以使用new关键字来创建类的实例。例如:

let john = new Person("John", 30);
john.greet(); 

这里我们创建了一个名为johnPerson类实例,并调用了它的greet方法。

类的访问修饰符

TypeScript提供了三种访问修饰符,用于控制类的属性和方法的访问权限,分别是publicprivateprotected

public

public是默认的访问修饰符。如果没有显式指定访问修饰符,类的属性和方法默认都是public,这意味着它们可以在类的内部和外部被访问。例如:

class Animal {
    public name: string;

    constructor(name: string) {
        this.name = name;
    }

    public makeSound() {
        console.log(`${this.name} makes a sound.`);
    }
}

let dog = new Animal("Buddy");
console.log(dog.name); 
dog.makeSound(); 

在上述代码中,name属性和makeSound方法都是public,所以可以在类外部通过对象实例进行访问。

private

private修饰的属性和方法只能在类的内部被访问,在类的外部无法直接访问。例如:

class Secret {
    private secretValue: number;

    constructor(value: number) {
        this.secretValue = value;
    }

    private printSecret() {
        console.log(`The secret value is ${this.secretValue}`);
    }

    public accessSecret() {
        this.printSecret(); 
    }
}

let secretObj = new Secret(42);
// console.log(secretObj.secretValue); // 报错,无法在类外部访问private属性
// secretObj.printSecret(); // 报错,无法在类外部访问private方法
secretObj.accessSecret(); 

在这个例子中,secretValue属性和printSecret方法都是private的,不能在类外部直接访问。但是我们可以通过类内部的public方法accessSecret来间接访问private的内容。

protected

protected修饰的属性和方法只能在类的内部以及子类中被访问。例如:

class Shape {
    protected color: string;

    constructor(color: string) {
        this.color = color;
    }

    protected describe() {
        console.log(`This shape is ${this.color}`);
    }
}

class Circle extends Shape {
    radius: number;

    constructor(color: string, radius: number) {
        super(color);
        this.radius = radius;
    }

    draw() {
        this.describe(); 
        console.log(`It has a radius of ${this.radius}`);
    }
}

let redCircle = new Circle("red", 5);
// console.log(redCircle.color); // 报错,无法在类外部访问protected属性
// redCircle.describe(); // 报错,无法在类外部访问protected方法
redCircle.draw(); 

在上述代码中,Shape类的color属性和describe方法是protected的。Circle类继承自Shape类,在Circle类内部可以访问Shape类的protected成员,如在draw方法中调用了describe方法。

类的继承

继承是面向对象编程的重要特性之一,它允许一个类继承另一个类的属性和方法。在TypeScript中,使用extends关键字来实现类的继承。

基本继承示例

class Vehicle {
    public brand: string;

    constructor(brand: string) {
        this.brand = brand;
    }

    public drive() {
        console.log(`Driving a ${this.brand} vehicle.`);
    }
}

class Car extends Vehicle {
    public model: string;

    constructor(brand: string, model: string) {
        super(brand);
        this.model = model;
    }

    public displayInfo() {
        console.log(`This is a ${this.brand} ${this.model}`);
    }
}

let myCar = new Car("Toyota", "Corolla");
myCar.drive(); 
myCar.displayInfo(); 

在这个例子中,Car类继承自Vehicle类。Car类不仅拥有自己的model属性和displayInfo方法,还继承了Vehicle类的brand属性和drive方法。在Car类的构造函数中,使用super关键字调用了Vehicle类的构造函数,以初始化从Vehicle类继承的brand属性。

重写方法

子类可以重写从父类继承的方法,以提供更特定的实现。例如:

class Animal {
    public name: string;

    constructor(name: string) {
        this.name = name;
    }

    public makeSound() {
        console.log(`${this.name} makes a generic sound.`);
    }
}

class Dog extends Animal {
    constructor(name: string) {
        super(name);
    }

    public makeSound() {
        console.log(`${this.name} barks.`);
    }
}

let myDog = new Dog("Max");
myDog.makeSound(); 

在上述代码中,Dog类继承自Animal类,并重写了makeSound方法。当调用myDog.makeSound()时,执行的是Dog类中重写后的方法,而不是Animal类中的原始方法。

抽象类

抽象类是一种不能被实例化的类,它主要用于为其他类提供一个通用的基类。抽象类可以包含抽象方法和具体方法。

定义抽象类

abstract class Shape {
    public color: string;

    constructor(color: string) {
        this.color = color;
    }

    public abstract getArea(): number;

    public describe() {
        console.log(`This shape is ${this.color}`);
    }
}

在上述代码中,Shape类被定义为抽象类,使用abstract关键字修饰。它包含一个抽象方法getArea,抽象方法没有具体的实现,只有方法签名,子类必须实现这个抽象方法。同时,Shape类还包含一个具体方法describe

继承抽象类

class Circle extends Shape {
    radius: number;

    constructor(color: string, radius: number) {
        super(color);
        this.radius = radius;
    }

    public getArea(): number {
        return Math.PI * this.radius * this.radius;
    }
}

let redCircle = new Circle("red", 5);
redCircle.describe(); 
console.log(`Area: ${redCircle.getArea()}`); 

Circle类继承自抽象类Shape,并实现了抽象方法getArea。这样Circle类就可以被实例化,并使用从Shape类继承的具体方法和自身实现的方法。

类与接口的关系

接口在TypeScript中用于定义对象的形状,而类可以实现接口,以确保类具有接口所定义的属性和方法。

类实现接口

interface Drawable {
    draw(): void;
}

class Rectangle implements Drawable {
    width: number;
    height: number;

    constructor(width: number, height: number) {
        this.width = width;
        this.height = height;
    }

    draw() {
        console.log(`Drawing a rectangle with width ${this.width} and height ${this.height}`);
    }
}

let rect = new Rectangle(10, 5);
rect.draw(); 

在这个例子中,Drawable接口定义了一个draw方法。Rectangle类实现了Drawable接口,因此必须提供draw方法的具体实现。

接口继承接口与类实现多重接口

接口可以继承其他接口,类也可以实现多个接口。例如:

interface Shape {
    getArea(): number;
}

interface Colorable {
    color: string;
}

interface ColoredShape extends Shape, Colorable {}

class Square implements ColoredShape {
    side: number;
    color: string;

    constructor(side: number, color: string) {
        this.side = side;
        this.color = color;
    }

    getArea(): number {
        return this.side * this.side;
    }
}

let blueSquare = new Square(4, "blue");
console.log(`Area of the blue square: ${blueSquare.getArea()}`); 
console.log(`Color of the blue square: ${blueSquare.color}`); 

在上述代码中,ColoredShape接口继承了ShapeColorable接口。Square类实现了ColoredShape接口,因此必须实现Shape接口的getArea方法,并拥有Colorable接口定义的color属性。

类的静态成员

类的静态成员是属于类本身而不是类的实例的属性和方法。使用static关键字来定义静态成员。

静态属性

class MathUtils {
    static PI: number = 3.14159;

    static calculateCircleArea(radius: number): number {
        return this.PI * radius * radius;
    }
}

console.log(MathUtils.PI); 
console.log(`Circle area: ${MathUtils.calculateCircleArea(5)}`); 

在这个例子中,PIMathUtils类的静态属性,calculateCircleArea是静态方法。我们可以通过类名直接访问静态成员,而不需要创建类的实例。

静态方法中的this

在静态方法中,this指向类本身,而不是类的实例。例如:

class Logger {
    static log(message: string) {
        console.log(`[${this.name}] ${message}`);
    }
}

class ConsoleLogger extends Logger {
    static name = "ConsoleLogger";
}

class FileLogger extends Logger {
    static name = "FileLogger";
}

ConsoleLogger.log("This is a console log"); 
FileLogger.log("This is a file log"); 

在上述代码中,Logger类的静态方法log使用this.name来输出日志的来源。当ConsoleLoggerFileLogger调用log方法时,this分别指向ConsoleLoggerFileLogger类,从而输出不同的日志前缀。

类的装饰器

类装饰器是TypeScript提供的一种元编程的方式,它可以在类定义时对类进行修改。装饰器本质上是一个函数,它接收类的构造函数作为参数,并可以返回一个新的构造函数或直接修改传入的构造函数。

简单的类装饰器示例

function Logger(constructor: Function) {
    console.log(`Class ${constructor.name} has been logged`);
}

@Logger
class MyClass {
    // 类的定义
}

在上述代码中,Logger是一个类装饰器。当MyClass类被定义时,Logger装饰器函数会被调用,并传入MyClass的构造函数,输出类的名称。

装饰器工厂

装饰器工厂是一个返回装饰器函数的函数。这样可以通过传递不同的参数来定制装饰器的行为。例如:

function LoggerWithPrefix(prefix: string) {
    return function (constructor: Function) {
        console.log(`${prefix}: Class ${constructor.name} has been logged`);
    };
}

@LoggerWithPrefix("INFO")
class AnotherClass {
    // 类的定义
}

在这个例子中,LoggerWithPrefix是一个装饰器工厂,它接收一个prefix参数,并返回一个装饰器函数。AnotherClass类使用了这个由装饰器工厂返回的装饰器,输出带有特定前缀的日志。

类装饰器修改类的行为

类装饰器还可以修改类的行为,例如添加新的方法。

function AddMethod(constructor: Function) {
    constructor.prototype.newMethod = function () {
        console.log("This is a new method added by the decorator");
    };
}

@AddMethod
class MyObject {
    // 类的定义
}

let obj = new MyObject();
obj.newMethod(); 

在上述代码中,AddMethod装饰器在MyObject类的原型上添加了一个新的方法newMethod,这样MyObject类的实例就可以调用这个新方法。

通过深入理解TypeScript的类和继承,我们可以更有效地利用面向对象编程的特性,编写出结构清晰、可维护性强的代码。从类的基本定义、访问修饰符的使用,到继承、抽象类、接口的配合,再到静态成员和装饰器等高级特性,每一个部分都在构建复杂而健壮的软件系统中发挥着重要作用。无论是开发小型项目还是大型企业级应用,熟练掌握这些知识都是非常必要的。在实际编程中,我们可以根据具体的需求和场景,灵活运用这些概念和技术,提升代码的质量和开发效率。