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

TypeScript类和继承的实现方式

2024-11-047.1k 阅读

TypeScript 类的基础定义与使用

在 TypeScript 中,类是面向对象编程的核心概念之一。类就像是一个蓝图,用于创建对象。我们通过定义类来描述对象的属性和行为。

首先,来看一个简单的类定义示例:

class Person {
    name: string;
    age: number;

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

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

在上述代码中:

  1. 属性定义:我们定义了两个属性 nameage,分别为字符串类型和数字类型。这些属性用于存储对象的相关数据。
  2. 构造函数constructor 是类的特殊方法,用于在创建对象时初始化对象的属性。这里接受两个参数 nameage,并将它们赋值给对应的对象属性。
  3. 方法定义greet 方法是一个实例方法,它返回一个包含对象 nameage 的问候字符串。

要使用这个类,我们可以这样创建对象:

let person1 = new Person('Alice', 30);
console.log(person1.greet()); 

类的访问修饰符

TypeScript 提供了几种访问修饰符,用于控制类的属性和方法的访问权限。

  1. public(公共的):这是默认的访问修饰符。被 public 修饰的属性和方法可以在类的内部和外部被访问。
class Animal {
    public name: string;

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

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

let dog = new Animal('Buddy');
console.log(dog.name); 
console.log(dog.makeSound()); 
  1. private(私有的):被 private 修饰的属性和方法只能在类的内部被访问。
class SecretData {
    private data: string;

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

    private printData() {
        console.log(this.data);
    }

    public showData() {
        this.printData();
    }
}

let secret = new SecretData('Confidential information');
// console.log(secret.data); // 这将导致编译错误,因为 data 是私有的
// secret.printData(); // 这也将导致编译错误,因为 printData 是私有的
secret.showData(); 
  1. protected(受保护的):被 protected 修饰的属性和方法可以在类的内部以及它的子类中被访问,但不能在类的外部被访问。
class Shape {
    protected color: string;

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

    protected getColor() {
        return this.color;
    }
}

class Circle extends Shape {
    radius: number;

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

    public describe() {
        return `This is a ${this.getColor()} circle with radius ${this.radius}.`;
    }
}

let circle = new Circle('red', 5);
// console.log(circle.color); // 这将导致编译错误,因为 color 是受保护的
// console.log(circle.getColor()); // 这也将导致编译错误,因为 getColor 是受保护的
console.log(circle.describe()); 

类的静态成员

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

class MathUtils {
    static PI: number = 3.14159;

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

console.log(MathUtils.PI); 
console.log(MathUtils.calculateCircleArea(5)); 

在上述代码中,PI 是一个静态属性,calculateCircleArea 是一个静态方法。我们通过类名直接访问静态成员,而不需要创建类的实例。

TypeScript 中的继承

继承是面向对象编程的重要特性之一,它允许我们创建一个新类(子类),从一个已有的类(父类)中获取属性和方法。在 TypeScript 中,使用 extends 关键字来实现继承。

class Vehicle {
    brand: string;
    model: string;

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

    startEngine() {
        return `The ${this.brand} ${this.model}'s engine has started.`;
    }
}

class Car extends Vehicle {
    numDoors: number;

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

    showDetails() {
        return `This ${this.brand} ${this.model} has ${this.numDoors} doors.`;
    }
}

let myCar = new Car('Toyota', 'Corolla', 4);
console.log(myCar.startEngine()); 
console.log(myCar.showDetails()); 

在这个例子中:

  1. 定义父类Vehicle 类是父类,它有 brandmodel 两个属性,以及 startEngine 方法。
  2. 定义子类Car 类通过 extends 关键字继承自 Vehicle 类。它不仅拥有 Vehicle 类的属性和方法,还定义了自己特有的 numDoors 属性和 showDetails 方法。
  3. 调用父类构造函数:在 Car 类的构造函数中,我们使用 super 关键字调用父类的构造函数,以初始化从父类继承的属性。

重写父类方法

子类可以重写父类中定义的方法,以提供更具体或不同的实现。

class Animal {
    name: string;

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

    makeSound() {
        return `${this.name} makes a generic sound.`;
    }
}

class Dog extends Animal {
    breed: string;

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

    makeSound() {
        return `${this.name} (a ${this.breed}) barks.`;
    }
}

let myDog = new Dog('Buddy', 'Golden Retriever');
console.log(myDog.makeSound()); 

在上述代码中,Dog 类重写了 Animal 类的 makeSound 方法,提供了更具体的狗叫的实现。

抽象类和抽象方法

抽象类是一种不能被实例化的类,它主要用于作为其他类的基类。抽象类可以包含抽象方法,抽象方法是只有声明而没有实现的方法,子类必须重写这些抽象方法。

abstract class Shape {
    abstract calculateArea(): number;
}

class Rectangle extends Shape {
    width: number;
    height: number;

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

    calculateArea() {
        return this.width * this.height;
    }
}

class Circle extends Shape {
    radius: number;

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

    calculateArea() {
        return Math.PI * this.radius * this.radius;
    }
}

let rectangle = new Rectangle(5, 10);
let circle = new Circle(7);

console.log(rectangle.calculateArea()); 
console.log(circle.calculateArea()); 

在上述代码中:

  1. 抽象类定义Shape 是一个抽象类,它包含一个抽象方法 calculateArea
  2. 子类实现RectangleCircle 类继承自 Shape 类,并分别实现了 calculateArea 方法,以计算各自的面积。

多重继承的替代方案 - 混入(Mixins)

在 TypeScript 中,不像一些其他语言那样直接支持多重继承,但可以通过混入(Mixins)模式来实现类似的功能。混入是一种将多个类的功能合并到一个类中的技术。

// 定义混入类
class Logger {
    log(message: string) {
        console.log(`[LOG] ${message}`);
    }
}

class Timestamp {
    getTimestamp() {
        return new Date().toISOString();
    }
}

// 定义混入函数
function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            if (name!== 'constructor') {
                derivedCtor.prototype[name] = baseCtor.prototype[name];
            }
        });
    });
}

// 使用混入
class MyClass {
    data: string;

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

applyMixins(MyClass, [Logger, Timestamp]);

let myObj = new MyClass('Some data');
myObj.log(`Data at ${myObj.getTimestamp()}: ${myObj.data}`); 

在上述代码中:

  1. 定义混入类Logger 类提供日志记录功能,Timestamp 类提供获取时间戳功能。
  2. 定义混入函数applyMixins 函数将多个混入类的方法合并到目标类的原型上。
  3. 使用混入MyClass 通过 applyMixins 函数混入了 LoggerTimestamp 的功能,从而可以使用它们的方法。

类与接口的关系

在 TypeScript 中,类和接口紧密相关。接口可以用于描述类的形状,即类应该具有哪些属性和方法。

  1. 类实现接口:一个类可以实现一个或多个接口,以保证满足接口定义的契约。
interface Printable {
    print(): void;
}

class Book implements Printable {
    title: string;

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

    print() {
        console.log(`Book title: ${this.title}`);
    }
}

let book = new Book('TypeScript in Action');
book.print(); 

在上述代码中,Book 类实现了 Printable 接口,必须提供 print 方法的实现。

  1. 接口继承接口:接口可以继承其他接口,形成更复杂的接口结构。
interface Shape {
    area(): number;
}

interface Colorable {
    color: string;
}

interface ColoredShape extends Shape, Colorable {
    // 这里可以添加更多属性或方法
}

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

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

    area() {
        return this.sideLength * this.sideLength;
    }
}

let square = new Square(5, 'blue');
console.log(`The ${square.color} square has an area of ${square.area()}.`); 

在这个例子中,ColoredShape 接口继承自 ShapeColorable 接口,Square 类实现了 ColoredShape 接口,需要实现 area 方法并提供 color 属性。

类的类型兼容性

在 TypeScript 中,类的类型兼容性是基于结构的。这意味着如果两个类具有兼容的结构,它们在类型上就是兼容的。

class Point {
    x: number;
    y: number;
}

class Position {
    x: number;
    y: number;
}

let point: Point = new Point();
let position: Position = new Position();

point = position; 
position = point; 

在上述代码中,Point 类和 Position 类具有相同的结构(都有 xy 属性),因此它们在类型上是兼容的,可以相互赋值。

然而,如果类的结构不同,就会出现类型不兼容的情况。

class Circle {
    radius: number;
}

class Rectangle {
    width: number;
    height: number;
}

let circle: Circle = new Circle();
let rectangle: Rectangle = new Rectangle();

// circle = rectangle; // 这将导致编译错误,因为结构不兼容
// rectangle = circle; // 这也将导致编译错误,因为结构不兼容

类的泛型

泛型是 TypeScript 中强大的特性,它允许我们在定义类时使用类型参数,使类能够处理不同类型的数据,同时保持类型安全。

class Box<T> {
    value: T;

    constructor(value: T) {
        this.value = value;
    }

    getValue() {
        return this.value;
    }
}

let numberBox = new Box<number>(42);
let stringBox = new Box<string>('Hello, TypeScript');

console.log(numberBox.getValue()); 
console.log(stringBox.getValue()); 

在上述代码中:

  1. 定义泛型类Box 类使用类型参数 Tvalue 属性的类型为 T,构造函数接受一个类型为 T 的参数。
  2. 使用泛型类:我们可以通过在类名后指定具体类型(如 Box<number>Box<string>)来创建不同类型的 Box 实例,分别用于存储数字和字符串。

类的装饰器

装饰器是一种特殊的声明,可以附加到类、方法、属性或参数上,用于修改或增强它们的行为。在 TypeScript 中,装饰器是一个函数,它接受目标(类、方法等)作为参数,并返回修改后的目标。

  1. 类装饰器:类装饰器应用于类的定义。
function logClass(target: Function) {
    console.log(`Class ${target.name} has been logged.`);
    return target;
}

@logClass
class MyClass {
    message: string;

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

    showMessage() {
        console.log(this.message);
    }
}

let myObj = new MyClass('Hello from MyClass');
myObj.showMessage(); 

在上述代码中,logClass 是一个类装饰器,它在类定义时打印类名。@logClass 语法将装饰器应用到 MyClass 上。

  1. 方法装饰器:方法装饰器应用于类的方法。
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    let originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        console.log(`Calling method ${propertyKey} with arguments:`, args);
        let result = originalMethod.apply(this, args);
        console.log(`Method ${propertyKey} returned:`, result);
        return result;
    };
    return descriptor;
}

class MathOperations {
    @logMethod
    add(a: number, b: number) {
        return a + b;
    }
}

let mathOps = new MathOperations();
mathOps.add(3, 5); 

在这个例子中,logMethod 是一个方法装饰器,它在方法调用前后打印日志信息,记录传入的参数和返回的结果。

  1. 属性装饰器:属性装饰器应用于类的属性。
function uppercase(target: any, propertyKey: string) {
    let value = target[propertyKey];
    const getter = function() {
        return value.toUpperCase();
    };
    const setter = function(newValue: string) {
        value = newValue;
    };
    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class Person {
    @uppercase
    name: string;

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

let person = new Person('alice');
console.log(person.name); 

在上述代码中,uppercase 是一个属性装饰器,它将属性值转换为大写形式。当访问 name 属性时,会返回大写的字符串。

  1. 参数装饰器:参数装饰器应用于类方法的参数。
function validateNumber(target: any, propertyKey: string, parameterIndex: number) {
    return function(...args: any[]) {
        if (typeof args[parameterIndex]!== 'number') {
            throw new Error(`Argument at index ${parameterIndex} must be a number.`);
        }
        return target.apply(this, args);
    };
}

class Calculator {
    @validateNumber
    multiply(a: number, b: number) {
        return a * b;
    }
}

let calculator = new Calculator();
console.log(calculator.multiply(3, 5)); 
// calculator.multiply(3, 'five'); // 这将抛出错误

在这个例子中,validateNumber 是一个参数装饰器,它在方法调用前验证指定参数是否为数字类型,如果不是则抛出错误。

通过上述对 TypeScript 类和继承的各个方面的详细介绍,包括类的基础定义、访问修饰符、静态成员、继承、重写方法、抽象类与方法、混入、类与接口关系、类型兼容性、泛型以及装饰器等,希望读者对 TypeScript 在面向对象编程方面的强大功能有了更深入的理解和掌握。在实际开发中,可以根据具体需求灵活运用这些特性,构建出健壮、可维护且类型安全的代码。