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

TypeScript类的高级用法与设计模式实践

2022-12-026.8k 阅读

一、TypeScript 类的继承与多态

在 TypeScript 中,类的继承是实现代码复用和多态性的重要手段。通过继承,一个类可以获取另一个类的属性和方法,同时还能进行扩展和重写。

1.1 基础继承

首先,我们来看一个简单的继承示例。假设我们有一个 Animal 类,它具有基本的属性和方法:

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

然后,我们可以创建一个继承自 AnimalDog 类:

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

在上述代码中,Dog 类通过 extends 关键字继承了 Animal 类。在 Dog 类的构造函数中,我们使用 super 关键字调用了 Animal 类的构造函数,以初始化继承自父类的 name 属性。同时,Dog 类重写了 speak 方法,实现了自己特有的行为。

1.2 多态性

多态性允许我们使用相同的接口来处理不同类型的对象。结合上面的例子,我们可以定义一个函数,它接受 Animal 类型的参数,并调用其 speak 方法:

function makeSound(animal: Animal) {
    animal.speak();
}

const animal = new Animal('Generic Animal');
const dog = new Dog('Buddy', 'Golden Retriever');

makeSound(animal);
makeSound(dog);

在这个例子中,makeSound 函数可以接受 Animal 及其子类 Dog 的实例。当我们调用 makeSound 并传入不同类型的对象时,会根据对象的实际类型调用相应的 speak 方法,这就是多态性的体现。

二、TypeScript 类的访问修饰符与封装

访问修饰符是 TypeScript 类的重要特性,它们用于控制类的属性和方法的访问权限,实现封装的概念。

2.1 public 修饰符

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

class PublicExample {
    public data: string;
    public display() {
        console.log(this.data);
    }
}

const publicObj = new PublicExample();
publicObj.data = 'Public Data';
publicObj.display();

2.2 private 修饰符

private 修饰符表示属性或方法只能在类的内部访问,子类和类的实例外部都无法访问。

class PrivateExample {
    private secretData: string;
    constructor(secret: string) {
        this.secretData = secret;
    }
    private internalMethod() {
        console.log(this.secretData);
    }
    public accessSecret() {
        this.internalMethod();
    }
}

const privateObj = new PrivateExample('Secret Information');
// privateObj.secretData; // 报错,无法在类外部访问 private 属性
// privateObj.internalMethod(); // 报错,无法在类外部访问 private 方法
privateObj.accessSecret();

在上面的代码中,secretDatainternalMethod 都是 private 的,只能通过类内部的 public 方法 accessSecret 来间接访问。

2.3 protected 修饰符

protected 修饰符与 private 类似,但它允许属性和方法在类的内部以及子类中访问,而在类的实例外部无法访问。

class BaseClass {
    protected protectedData: string;
    protected protectedMethod() {
        console.log(this.protectedData);
    }
    constructor(data: string) {
        this.protectedData = data;
    }
}

class SubClass extends BaseClass {
    accessProtected() {
        this.protectedMethod();
    }
}

const subObj = new SubClass('Protected Data');
// subObj.protectedData; // 报错,无法在类外部访问 protected 属性
// subObj.protectedMethod(); // 报错,无法在类外部访问 protected 方法
subObj.accessProtected();

在这个例子中,SubClass 继承自 BaseClass,可以访问 BaseClass 中的 protected 属性和方法。

三、TypeScript 类的抽象类与接口

抽象类和接口是 TypeScript 中用于定义类型契约的重要工具,它们在设计大型应用程序时非常有用。

3.1 抽象类

抽象类是一种不能被实例化的类,它通常作为其他类的基类,包含一些抽象方法。抽象方法只有声明,没有实现,必须在子类中被重写。

abstract class Shape {
    abstract area(): number;
    abstract perimeter(): number;
}

class Circle extends Shape {
    radius: number;
    constructor(radius: number) {
        super();
        this.radius = radius;
    }
    area(): number {
        return Math.PI * this.radius * this.radius;
    }
    perimeter(): number {
        return 2 * Math.PI * this.radius;
    }
}

class Rectangle extends Shape {
    width: number;
    height: number;
    constructor(width: number, height: number) {
        super();
        this.width = width;
        this.height = height;
    }
    area(): number {
        return this.width * this.height;
    }
    perimeter(): number {
        return 2 * (this.width + this.height);
    }
}

在上述代码中,Shape 是一个抽象类,它定义了 areaperimeter 两个抽象方法。CircleRectangle 类继承自 Shape,并实现了这些抽象方法。

3.2 接口

接口用于定义对象的形状,它只包含属性和方法的声明,不包含实现。类可以实现一个或多个接口,以确保满足接口定义的契约。

interface Printable {
    print(): void;
}

class Book implements Printable {
    title: string;
    constructor(title: string) {
        this.title = title;
    }
    print(): void {
        console.log(`Book title: ${this.title}`);
    }
}

class Magazine implements Printable {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    print(): void {
        console.log(`Magazine name: ${this.name}`);
    }
}

在这个例子中,Printable 接口定义了 print 方法。BookMagazine 类实现了 Printable 接口,从而保证它们都具有 print 方法。

四、TypeScript 类与设计模式实践

设计模式是在软件开发中反复出现的问题的通用解决方案。下面我们将探讨如何在 TypeScript 类中应用一些常见的设计模式。

4.1 单例模式

单例模式确保一个类只有一个实例,并提供一个全局访问点。在 TypeScript 中,可以通过以下方式实现单例模式:

class Singleton {
    private static instance: Singleton;
    private constructor() {}
    public static getInstance(): Singleton {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
    public doSomething() {
        console.log('Singleton is doing something.');
    }
}

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true

在上述代码中,Singleton 类的构造函数是 private 的,防止外部直接实例化。通过 getInstance 静态方法来获取单例实例,如果实例不存在则创建一个,确保始终返回同一个实例。

4.2 工厂模式

工厂模式提供了一种创建对象的方式,将对象的创建和使用分离。下面是一个简单的工厂模式示例:

abstract class Product {
    abstract operation(): string;
}

class ConcreteProduct1 extends Product {
    operation(): string {
        return 'Result of ConcreteProduct1';
    }
}

class ConcreteProduct2 extends Product {
    operation(): string {
        return 'Result of ConcreteProduct2';
    }
}

class Factory {
    createProduct(type: string): Product {
        if (type === 'product1') {
            return new ConcreteProduct1();
        } else if (type === 'product2') {
            return new ConcreteProduct2();
        }
        throw new Error('Invalid product type');
    }
}

const factory = new Factory();
const product1 = factory.createProduct('product1');
const product2 = factory.createProduct('product2');
console.log(product1.operation());
console.log(product2.operation());

在这个例子中,Factory 类负责根据传入的类型创建不同的 Product 实例。Product 是一个抽象类,ConcreteProduct1ConcreteProduct2 是它的具体实现。

4.3 观察者模式

观察者模式定义了一种一对多的依赖关系,当一个对象状态发生改变时,所有依赖它的对象都会收到通知并自动更新。

class Subject {
    private observers: Observer[] = [];
    private state: number;

    public attach(observer: Observer) {
        this.observers.push(observer);
    }

    public detach(observer: Observer) {
        const index = this.observers.indexOf(observer);
        if (index!== -1) {
            this.observers.splice(index, 1);
        }
    }

    public setState(state: number) {
        this.state = state;
        this.notify();
    }

    private notify() {
        this.observers.forEach(observer => observer.update(this.state));
    }
}

interface Observer {
    update(state: number): void;
}

class ConcreteObserver implements Observer {
    private name: string;
    constructor(name: string) {
        this.name = name;
    }
    update(state: number) {
        console.log(`${this.name} received update: ${state}`);
    }
}

const subject = new Subject();
const observer1 = new ConcreteObserver('Observer1');
const observer2 = new ConcreteObserver('Observer2');

subject.attach(observer1);
subject.attach(observer2);

subject.setState(42);

在上述代码中,Subject 类维护一个观察者列表,当状态改变时通过 notify 方法通知所有观察者。Observer 是一个接口,定义了 update 方法,具体的观察者类 ConcreteObserver 实现了这个接口。

五、TypeScript 类的 Mixin 模式

Mixin 模式是一种在不使用继承的情况下复用代码的技术。它通过将多个函数或类的功能混合到一个新的类中。

5.1 简单 Mixin 示例

// Mixin 函数
function LoggingMixin(BaseClass: any) {
    return class extends BaseClass {
        log(message: string) {
            console.log(`${new Date().toISOString()} - ${message}`);
        }
    };
}

function TimestampMixin(BaseClass: any) {
    return class extends BaseClass {
        timestamp: Date;
        constructor() {
            super();
            this.timestamp = new Date();
        }
    };
}

class MyClass {}

const EnhancedClass = TimestampMixin(LoggingMixin(MyClass));

const instance = new EnhancedClass();
instance.log('This is a log message');
console.log(instance.timestamp);

在这个例子中,LoggingMixinTimestampMixin 是两个 Mixin 函数,它们接受一个基类并返回一个新的类,这个新类包含了 Mixin 函数中定义的额外功能。通过将这些 Mixin 函数依次应用到 MyClass 上,EnhancedClass 就获得了日志记录和时间戳的功能。

5.2 使用类型定义的 Mixin

为了更好地使用类型系统,我们可以为 Mixin 定义类型。

interface Logging {
    log(message: string): void;
}

function LoggingMixin<T extends new (...args: any[]) => any>(BaseClass: T): T & { new (...args: any[]): Logging & InstanceType<T> } {
    return class extends BaseClass {
        log(message: string) {
            console.log(`${new Date().toISOString()} - ${message}`);
        }
    } as T & { new (...args: any[]): Logging & InstanceType<T> };
}

interface Timestamped {
    timestamp: Date;
}

function TimestampMixin<T extends new (...args: any[]) => any>(BaseClass: T): T & { new (...args: any[]): Timestamped & InstanceType<T> } {
    return class extends BaseClass {
        timestamp: Date;
        constructor() {
            super();
            this.timestamp = new Date();
        }
    } as T & { new (...args: any[]): Timestamped & InstanceType<T> };
}

class MyBaseClass {}

const MyEnhancedClass = TimestampMixin(LoggingMixin(MyBaseClass));

const myInstance = new MyEnhancedClass();
myInstance.log('Mixin with types');
console.log(myInstance.timestamp);

在这个改进版本中,我们定义了 LoggingTimestamped 接口来描述 Mixin 提供的功能类型。通过对 Mixin 函数的类型定义,确保了在使用 Mixin 时类型的正确性。

六、TypeScript 类的装饰器

装饰器是一种特殊的声明,可以附加到类、方法、属性或参数上,用于修改或增强它们的行为。

6.1 类装饰器

类装饰器应用于类的定义。它接受一个参数,即被装饰的类的构造函数。

function classDecorator(target: Function) {
    console.log('Class Decorator called on', target.name);
    return class extends target {
        newMethod() {
            console.log('This is a new method added by the decorator.');
        }
    };
}

@classDecorator
class DecoratedClass {
    originalMethod() {
        console.log('This is the original method.');
    }
}

const decoratedInstance = new DecoratedClass();
decoratedInstance.originalMethod();
decoratedInstance.newMethod();

在上述代码中,classDecorator 是一个类装饰器,它在被装饰的 DecoratedClass 定义时被调用,并返回一个新的类,这个新类包含了额外的 newMethod

6.2 方法装饰器

方法装饰器应用于类的方法。它接受三个参数:目标对象、属性名和属性描述符。

function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log('Before method execution');
        const result = originalMethod.apply(this, args);
        console.log('After method execution');
        return result;
    };
    return descriptor;
}

class MethodDecoratedClass {
    @methodDecorator
    myMethod() {
        console.log('Inside myMethod');
    }
}

const methodDecoratedInstance = new MethodDecoratedClass();
methodDecoratedInstance.myMethod();

在这个例子中,methodDecorator 是一个方法装饰器,它在 myMethod 被调用前后添加了日志输出,修改了方法的行为。

6.3 属性装饰器

属性装饰器应用于类的属性。它接受两个参数:目标对象和属性名。

function propertyDecorator(target: any, propertyKey: string) {
    let value: any;
    const getter = function () {
        return value;
    };
    const setter = function (newValue: any) {
        console.log(`Setting property ${propertyKey} to ${newValue}`);
        value = newValue;
    };
    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class PropertyDecoratedClass {
    @propertyDecorator
    myProperty: string;
    constructor(value: string) {
        this.myProperty = value;
    }
}

const propertyDecoratedInstance = new PropertyDecoratedClass('Initial Value');
console.log(propertyDecoratedInstance.myProperty);
propertyDecoratedInstance.myProperty = 'New Value';

在上述代码中,propertyDecorator 是一个属性装饰器,它为 myProperty 属性添加了自定义的访问器,在设置属性值时输出日志。

七、TypeScript 类在大型项目中的最佳实践

在大型项目中,合理使用 TypeScript 类可以提高代码的可维护性、可扩展性和可测试性。

7.1 模块与类的组织

将相关的类组织到模块中,每个模块有明确的职责。例如,将数据访问层的类放在一个模块中,业务逻辑层的类放在另一个模块中。

// user.ts
export class User {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}

// userService.ts
import { User } from './user';

export class UserService {
    private users: User[] = [];
    addUser(user: User) {
        this.users.push(user);
    }
    getUsers() {
        return this.users;
    }
}

通过这种方式,代码结构更加清晰,不同模块之间的依赖关系也更容易管理。

7.2 依赖注入

使用依赖注入来解耦类之间的依赖关系,提高代码的可测试性和可维护性。

class Logger {
    log(message: string) {
        console.log(message);
    }
}

class Database {
    connect() {
        console.log('Connected to database');
    }
}

class Application {
    private logger: Logger;
    private database: Database;
    constructor(logger: Logger, database: Database) {
        this.logger = logger;
        this.database = database;
    }
    start() {
        this.logger.log('Application starting');
        this.database.connect();
    }
}

const logger = new Logger();
const database = new Database();
const app = new Application(logger, database);
app.start();

在这个例子中,Application 类通过构造函数接受 LoggerDatabase 的实例,而不是在内部创建它们,这样可以方便地替换这些依赖,进行单元测试。

7.3 测试驱动开发(TDD)

在编写类的代码之前,先编写测试用例。例如,使用 Jest 来测试一个简单的数学运算类:

// mathOperations.ts
export class MathOperations {
    add(a: number, b: number) {
        return a + b;
    }
    subtract(a: number, b: number) {
        return a - b;
    }
}

// mathOperations.test.ts
import { MathOperations } from './mathOperations';

describe('MathOperations', () => {
    let mathOperations: MathOperations;
    beforeEach(() => {
        mathOperations = new MathOperations();
    });

    test('add should return the sum of two numbers', () => {
        expect(mathOperations.add(2, 3)).toBe(5);
    });

    test('subtract should return the difference of two numbers', () => {
        expect(mathOperations.subtract(5, 3)).toBe(2);
    });
});

通过 TDD,可以确保类的功能正确,并且在后续修改代码时能及时发现潜在的问题。

7.4 代码复用与避免过度继承

在项目中,尽量复用已有的类和代码,而不是过度依赖继承。可以使用组合和 Mixin 等技术来实现代码复用,以避免继承带来的复杂性和脆弱性。例如,通过 Mixin 为多个类添加相同的日志记录功能,而不是通过继承一个包含日志功能的基类。

八、TypeScript 类与 JavaScript 类的对比与互操作性

TypeScript 是 JavaScript 的超集,支持 JavaScript 类的所有特性,并在此基础上增加了类型系统。

8.1 语法差异

TypeScript 类增加了类型注解,使得代码更加明确和可维护。例如:

class TypeScriptClass {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    greet(): void {
        console.log(`Hello, ${this.name}`);
    }
}

而 JavaScript 类没有类型注解:

class JavaScriptClass {
    constructor(name) {
        this.name = name;
    }
    greet() {
        console.log(`Hello, ${this.name}`);
    }
}

8.2 编译与运行

TypeScript 代码需要先编译成 JavaScript 代码才能在 JavaScript 运行环境中执行。在编译过程中,TypeScript 编译器会检查类型错误,有助于在开发阶段发现问题。

8.3 互操作性

TypeScript 可以很好地与 JavaScript 代码互操作。可以在 TypeScript 项目中引入 JavaScript 模块,也可以在 JavaScript 项目中逐步引入 TypeScript 代码。例如,在 TypeScript 中引入一个 JavaScript 模块:

// main.ts
import * as jsModule from './jsModule.js';
jsModule.doSomething();

在 JavaScript 模块 jsModule.js 中:

exports.doSomething = function () {
    console.log('This is a JavaScript function');
};

同样,也可以在 JavaScript 中使用 TypeScript 编译后的代码,只要遵循 JavaScript 的语法规则即可。

九、总结 TypeScript 类的高级用法要点

  1. 继承与多态:通过 extends 关键字实现继承,利用多态性以统一接口处理不同类型对象,提高代码复用和灵活性。
  2. 访问修饰符publicprivateprotected 用于控制属性和方法的访问权限,实现封装,保护数据安全和代码结构清晰。
  3. 抽象类与接口:抽象类定义抽象方法,强制子类实现特定行为;接口定义对象形状,类通过实现接口保证满足特定契约。
  4. 设计模式实践:单例模式确保类仅有一个实例;工厂模式分离对象创建与使用;观察者模式实现对象间一对多依赖关系。
  5. Mixin 模式:在不使用继承的情况下复用代码,通过 Mixin 函数将多个功能混合到一个类中。
  6. 装饰器:类、方法、属性装饰器分别用于修改类、方法、属性的行为,增强代码的可维护性和扩展性。
  7. 大型项目最佳实践:合理组织模块与类,使用依赖注入解耦依赖,采用测试驱动开发确保代码质量,避免过度继承并注重代码复用。
  8. 与 JavaScript 的关系:TypeScript 是 JavaScript 超集,增加类型系统,可与 JavaScript 良好互操作,编译后可在 JavaScript 环境运行。