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

TypeScript面向对象编程的核心概念与实践

2021-01-183.7k 阅读

面向对象编程基础概念

类(Class)

在 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.`;
    }
}

// 创建 Person 类的实例
let person1 = new Person("Alice", 30);
console.log(person1.greet()); 

在上述代码中,我们定义了一个 Person 类,它有两个属性 nameage,以及一个构造函数 constructor 用于初始化这些属性。同时,还定义了一个 greet 方法来返回问候语。通过 new 关键字创建了 Person 类的实例 person1,并调用 greet 方法输出问候语。

封装(Encapsulation)

封装是面向对象编程的重要特性之一,它将数据和操作数据的方法包装在一起,对外隐藏内部的实现细节,只提供必要的接口来访问和操作数据。在 TypeScript 中,我们可以通过访问修饰符来实现封装。

  1. public(公共的):默认的访问修饰符,类的属性和方法在类内部、子类以及类的实例中都可以访问。
  2. private(私有的):标记为 private 的属性和方法只能在类的内部访问,子类和类的实例都无法访问。
  3. protected(受保护的):与 private 类似,但标记为 protected 的属性和方法可以在类的内部以及子类中访问。
class BankAccount {
    private accountNumber: string;
    private balance: number;

    constructor(accountNumber: string, initialBalance: number) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }

    // 公共方法,用于存款
    deposit(amount: number) {
        if (amount > 0) {
            this.balance += amount;
            return true;
        }
        return false;
    }

    // 公共方法,用于取款
    withdraw(amount: number) {
        if (amount > 0 && amount <= this.balance) {
            this.balance -= amount;
            return true;
        }
        return false;
    }

    // 公共方法,用于获取余额
    getBalance() {
        return this.balance;
    }
}

let account = new BankAccount("1234567890", 1000);
console.log(account.deposit(500)); 
console.log(account.withdraw(300)); 
console.log(account.getBalance()); 
// console.log(account.accountNumber);  // 这行代码会报错,因为 accountNumber 是私有的

在这个 BankAccount 类中,accountNumberbalance 属性被声明为 private,外部无法直接访问。通过提供 depositwithdrawgetBalance 这些公共方法,外部代码可以安全地与账户进行交互,实现了数据的封装。

继承(Inheritance)

继承允许一个类(子类)从另一个类(父类)继承属性和方法,从而实现代码的复用和扩展。子类可以重写父类的方法,以提供更具体的实现。在 TypeScript 中,使用 extends 关键字来实现继承。

class Animal {
    name: string;

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

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

class Dog extends Animal {
    breed: string;

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

    speak() {
        return `${this.name} barks.`;
    }
}

let dog = new Dog("Buddy", "Golden Retriever");
console.log(dog.speak()); 

在上述代码中,Dog 类继承自 Animal 类。Dog 类不仅拥有从 Animal 类继承的 name 属性和 speak 方法,还添加了自己特有的 breed 属性,并重写了 speak 方法以提供狗叫的具体实现。通过 super 关键字调用父类的构造函数来初始化继承的属性。

多态(Polymorphism)

多态是指同一个方法在不同的对象上表现出不同的行为。在 TypeScript 中,多态主要通过继承和方法重写来实现。当子类重写父类的方法时,不同子类的实例调用相同的方法名会产生不同的行为。

class Shape {
    calculateArea(): number {
        return 0;
    }
}

class Circle extends Shape {
    radius: number;

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

    calculateArea(): 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;
    }

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

let circle = new Circle(5);
let rectangle = new Rectangle(4, 6);

let shapes: Shape[] = [circle, rectangle];
for (let shape of shapes) {
    console.log(shape.calculateArea()); 
}

在这个例子中,Shape 类定义了一个 calculateArea 方法,CircleRectangle 类继承自 Shape 类并分别重写了 calculateArea 方法以计算各自的面积。通过将不同子类的实例放入 Shape 类型的数组中,并遍历调用 calculateArea 方法,体现了多态性,不同形状的对象计算出不同的面积。

TypeScript 中的接口与抽象类

接口(Interface)

接口在 TypeScript 中用于定义对象的形状(结构),它只定义属性和方法的签名,而不包含具体的实现。接口可以被类实现,一个类可以实现多个接口,这使得 TypeScript 具有类似多重继承的能力。

interface Drawable {
    draw(): void;
}

interface Resizable {
    resize(width: number, height: number): void;
}

class Rectangle implements Drawable, Resizable {
    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}`);
    }

    resize(width: number, height: number) {
        this.width = width;
        this.height = height;
        console.log(`Resized rectangle to width ${this.width} and height ${this.height}`);
    }
}

let rectangle = new Rectangle(4, 6);
rectangle.draw(); 
rectangle.resize(8, 10); 

在上述代码中,我们定义了 DrawableResizable 两个接口,分别定义了 drawresize 方法的签名。Rectangle 类实现了这两个接口,并提供了具体的方法实现。通过实现多个接口,Rectangle 类具备了多种行为。

抽象类(Abstract Class)

抽象类是一种不能被实例化的类,它通常作为其他类的基类,包含一些抽象方法(只有声明,没有实现)和具体方法。抽象方法必须在子类中被重写。抽象类通过 abstract 关键字来定义。

abstract class Vehicle {
    brand: string;

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

    abstract drive(): void;

    honk() {
        console.log(`${this.brand} vehicle honks.`);
    }
}

class Car extends Vehicle {
    model: string;

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

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

class Motorcycle extends Vehicle {
    type: string;

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

    drive() {
        console.log(`Riding a ${this.brand} ${this.type} motorcycle`);
    }
}

let car = new Car("Toyota", "Corolla");
let motorcycle = new Motorcycle("Honda", "CBR");

car.drive(); 
car.honk(); 
motorcycle.drive(); 
motorcycle.honk(); 

在这个例子中,Vehicle 是一个抽象类,它有一个抽象方法 drive 和一个具体方法 honkCarMotorcycle 类继承自 Vehicle 类,并实现了抽象方法 drive。抽象类为子类提供了一个通用的结构和行为框架,子类根据自身特点进行具体实现。

高级面向对象特性

泛型(Generics)

泛型是 TypeScript 中一个强大的特性,它允许我们在定义函数、类或接口时使用类型参数,使得这些组件可以适用于多种类型,而不是特定的类型。这提高了代码的复用性和灵活性。

  1. 泛型函数
function identity<T>(arg: T): T {
    return arg;
}

let result1 = identity<number>(5); 
let result2 = identity<string>("hello"); 

在这个 identity 函数中,T 是一个类型参数,它可以代表任何类型。通过在调用函数时指定类型参数,我们可以让函数适用于不同的类型。 2. 泛型类

class Box<T> {
    value: T;

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

    getValue(): T {
        return this.value;
    }
}

let numberBox = new Box<number>(10);
let stringBox = new Box<string>("world");

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

Box 类是一个泛型类,类型参数 T 用于表示存储的值的类型。通过创建不同类型的 Box 实例,我们可以存储不同类型的数据。 3. 泛型接口

interface KeyValuePair<K, V> {
    key: K;
    value: V;
}

let pair1: KeyValuePair<string, number> = { key: "age", value: 30 };
let pair2: KeyValuePair<number, boolean> = { key: 1, value: true };

KeyValuePair 接口是一个泛型接口,通过类型参数 KV 来定义键和值的类型。不同的实例可以使用不同的类型组合。

装饰器(Decorators)

装饰器是一种在 TypeScript 中添加元数据和修改类、方法、属性等行为的方式。它们以 @ 符号开头,后面跟着装饰器函数。装饰器可以用于日志记录、权限验证、依赖注入等场景。

  1. 类装饰器
function Logger(target: Function) {
    console.log(`Class ${target.name} has been logged.`);
}

@Logger
class MyClass {
    message: string;

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

let myClass = new MyClass("Hello"); 

在上述代码中,Logger 是一个类装饰器,当 MyClass 类被定义时,装饰器函数会被调用并输出日志。 2. 方法装饰器

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 MathUtils {
    @LogMethod
    add(a: number, b: number) {
        return a + b;
    }
}

let mathUtils = new MathUtils();
console.log(mathUtils.add(3, 5)); 

LogMethod 是一个方法装饰器,它在方法调用前后添加了日志记录,记录方法名、参数和返回值。 3. 属性装饰器

function Readonly(target: any, propertyKey: string) {
    let value = target[propertyKey];
    Object.defineProperty(target, propertyKey, {
        get: function () {
            return value;
        },
        set: function (newValue) {
            console.log(`Cannot set value of ${propertyKey} as it is readonly.`);
        },
        enumerable: true,
        configurable: true
    });
}

class User {
    @Readonly
    username: string;

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

let user = new User("John");
// user.username = "Jane";  // 这行代码会触发警告,因为 username 是只读的

Readonly 是一个属性装饰器,它将属性设置为只读,尝试修改属性值时会输出警告。

混入(Mixins)

混入是一种将多个类的功能合并到一个类中的技术,它允许我们在不使用继承的情况下复用代码。在 TypeScript 中,可以通过函数来实现混入。

// 定义混入类
function LoggerMixin(Base: any) {
    return class extends Base {
        log(message: string) {
            console.log(`[${new Date().toISOString()}] ${message}`);
        }
    };
}

function TimestampMixin(Base: any) {
    return class extends Base {
        timestamp: number;

        constructor() {
            super();
            this.timestamp = Date.now();
        }
    };
}

// 使用混入创建新类
class MyClass {}

let MyNewClass = TimestampMixin(LoggerMixin(MyClass));

let instance = new MyNewClass();
instance.log("This is a logged message");
console.log(instance.timestamp); 

在这个例子中,LoggerMixinTimestampMixin 是两个混入函数,它们分别为类添加了日志记录和时间戳的功能。通过将这两个混入函数应用到 MyClass 上,创建了一个新的类 MyNewClass,它同时具备了日志记录和时间戳的特性。

实践中的面向对象编程

项目结构设计

在实际项目中,合理的项目结构设计对于代码的可维护性和扩展性至关重要。通常,我们会根据功能模块将代码组织成不同的文件夹和文件,每个文件定义相关的类、接口等。 例如,对于一个简单的电商项目,我们可以有如下的项目结构:

src/
├── models/
│   ├── Product.ts
│   ├── User.ts
│   └── Order.ts
├── services/
│   ├── ProductService.ts
│   ├── UserService.ts
│   └── OrderService.ts
├── controllers/
│   ├── ProductController.ts
│   ├── UserController.ts
│   └── OrderController.ts
├── main.ts

models 文件夹中定义数据模型类,如 ProductUserOrder 类,它们封装了业务数据的结构和相关操作。services 文件夹中的服务类负责处理业务逻辑,例如 ProductService 类可能包含获取产品列表、添加产品等方法。controllers 文件夹中的控制器类用于处理 HTTP 请求,将请求转发给相应的服务类,并返回响应数据。main.ts 作为项目的入口文件,负责启动应用程序并初始化相关模块。

依赖注入

依赖注入是一种设计模式,它通过将依赖关系从类中分离出来,由外部提供依赖,从而提高代码的可测试性和可维护性。在 TypeScript 中,可以使用第三方库如 inversify - js 来实现依赖注入。

  1. 安装 inversify - js
npm install inversify reflect - metadata
  1. 示例代码
import "reflect - metadata";
import { Container, injectable, inject } from "inversify";

// 定义接口
interface Logger {
    log(message: string): void;
}

// 实现 Logger 接口
@injectable()
class ConsoleLogger implements Logger {
    log(message: string) {
        console.log(message);
    }
}

// 定义依赖 Logger 的类
@injectable()
class MyService {
    private logger: Logger;

    constructor(@inject(Logger) logger: Logger) {
        this.logger = logger;
    }

    doWork() {
        this.logger.log("Starting work...");
        // 实际业务逻辑
        this.logger.log("Work completed.");
    }
}

// 创建容器并绑定依赖
let container = new Container();
container.bind<Logger>(Logger).to(ConsoleLogger);
container.bind<MyService>(MyService).to(MyService);

// 获取实例并调用方法
let myService = container.get<MyService>(MyService);
myService.doWork(); 

在上述代码中,MyService 类依赖于 Logger 接口。通过 inversify - jsinjectableinject 装饰器,我们将 ConsoleLogger 实例注入到 MyService 类中。这样,在测试 MyService 类时,可以很方便地替换 Logger 的实现,例如使用一个模拟的 Logger 来验证日志输出。

单元测试

单元测试是确保代码质量的重要手段,在面向对象编程中,我们可以使用测试框架如 Jest 来编写单元测试。

  1. 安装 Jest
npm install --save - dev jest
  1. 示例代码
// 被测试的类
class Calculator {
    add(a: number, b: number) {
        return a + b;
    }

    subtract(a: number, b: number) {
        return a - b;
    }
}

// 测试文件
describe('Calculator', () => {
    let calculator: Calculator;

    beforeEach(() => {
        calculator = new Calculator();
    });

    it('should add two numbers correctly', () => {
        expect(calculator.add(2, 3)).toBe(5);
    });

    it('should subtract two numbers correctly', () => {
        expect(calculator.subtract(5, 3)).toBe(2);
    });
});

在这个例子中,我们使用 Jestdescribeit 函数来定义测试套件和测试用例。beforeEach 函数在每个测试用例执行前创建 Calculator 类的实例。通过 expecttoBe 方法来断言方法的返回值是否符合预期,从而验证 Calculator 类的功能正确性。

通过以上对 TypeScript 面向对象编程核心概念与实践的介绍,包括基础概念、接口与抽象类、高级特性以及实际项目中的应用,希望能帮助开发者更好地利用 TypeScript 进行面向对象编程,构建健壮、可维护和可扩展的前端应用程序。在实际开发中,还需要不断积累经验,结合项目需求灵活运用这些知识,以提高开发效率和代码质量。同时,随着 TypeScript 的不断发展,新的特性和最佳实践也会不断涌现,开发者需要持续学习和跟进,以保持技术的先进性。例如,未来可能会在装饰器、泛型等方面有更强大的功能和更广泛的应用场景,这就需要我们密切关注官方文档和社区动态,及时将新的技术应用到项目中。在大型项目中,还需要考虑如何进行有效的代码组织和架构设计,以应对日益复杂的业务需求。可以借鉴一些成熟的设计模式和架构风格,如 MVC、MVVM、微前端等,结合 TypeScript 的面向对象特性,打造出高效、稳定的前端应用。在团队协作方面,良好的代码规范和文档编写习惯也是必不可少的,这有助于团队成员之间的沟通和代码的传承。总之,TypeScript 面向对象编程是前端开发中非常重要的一部分,值得我们深入学习和不断实践。