TypeScript系列03-类与面向对象编程

面向对象编程(OOP)是 TypeScript 的核心特性之一,它使得代码组织更加结构化且可维护。本文将深入探讨 TypeScript 中类的各个方面,从基础概念到高级应用,帮助你全面理解并有效运用面向对象的设计思想。本文涵盖:

  1. 访问修饰符 (public、private、protected)和抽象类提供了代码封装和继承的机制
  2. 静态成员支持类级别的属性和方法,在单例、工厂等设计模式中发挥重要作用
  3. 接口实现增强了代码的可重用性和类型安全,通过契约定义类必须符合的结构
  4. 装饰器提供了强大的元编程能力,用于以非侵入式方式扩展类和成员的功能

1. 类的定义与继承

1.1 访问修饰符(public, private, protected)

TypeScript 提供了三种访问修饰符来控制类成员的可见性:

  • public: 默认修饰符,表示成员可以在任何地方被访问
  • private: 限制成员只能在声明它的类内部访问
  • protected: 允许成员在声明它的类及其子类中访问

以下代码展示了这三种访问修饰符的实际使用:

typescript 复制代码
class Person {
  public name: string;
  private age: number;
  protected address: string;

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

  public introduce(): string {
    return `我叫 ${this.name},今年 ${this.age} 岁`; // 可以访问 private 成员
  }

  private getPrivateDetails(): string {
    return `年龄:${this.age}`; // 只能在 Person 类内部访问
  }

  protected getAddress(): string {
    return this.address; // 可以在 Person 类及其子类中访问
  }
}

class Employee extends Person {
  private employeeId: string;

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

  public getEmployeeInfo(): string {
    // this.age 会报错,因为 age 是 Person 类的 private 属性
    // return `员工 ${this.name},年龄 ${this.age},地址 ${this.address}`; // 错误

    // 但可以访问 protected 成员
    return `员工 ${this.name},地址 ${this.getAddress()},ID ${this.employeeId}`;
  }
}

const person = new Person("张三", 25, "北京市");
console.log(person.name); // 可以访问:public
// console.log(person.age); // 错误:private
// console.log(person.address); // 错误:protected
console.log(person.introduce()); // 可以访问:public 方法
// console.log(person.getPrivateDetails()); // 错误:private 方法
// console.log(person.getAddress()); // 错误:protected 方法

1.2 属性初始化与构造函数

TypeScript 提供了多种属性初始化方式:

构造函数参数属性

一种简洁的语法,允许在声明构造函数参数时同时声明和初始化类属性

typescript 复制代码
class User {
  // 简洁写法,自动创建并初始化属性
  constructor(
    public username: string,
    private password: string,
    protected email: string = "default@example.com" // 带默认值
  ) {}
  
  getInfo(): string {
    return `User: ${this.username}, Email: ${this.email}`;
  }
}

const user = new User("admin", "secret");
console.log(user.username); // "admin"
// console.log(user.password); // 错误:private 属性

属性初始化器

直接在类定义中初始化属性

typescript 复制代码
class Counter {
  count = 0; // 属性初始化器
  readonly maxValue = 100;
  private lastUpdated: Date = new Date();
  
  increment(): void {
    this.count++;
    this.lastUpdated = new Date();
    // this.maxValue++; // 错误:readonly 属性不可修改
  }
}

非空断言属性初始化

使用 ! 操作符向编译器保证属性会在使用前被初始化

typescript 复制代码
class AsyncData {
  data!: string[]; // 使用非空断言,承诺在使用前会初始化
  
  async loadData(): Promise<void> {
    // 模拟异步加载
    this.data = await Promise.resolve(["item1", "item2"]);
  }
  
  // 如果在调用 processData 前没有调用 loadData,可能会导致运行时错误
  processData(): number {
    return this.data.length;
  }
}

1.3 抽象类与方法

抽象类作为其他类的基类,不能被直接实例化,常用于定义通用接口和部分实现:

typescript 复制代码
abstract class Shape {
  constructor(protected color: string) {}
  
  // 抽象方法必须在派生类中实现
  abstract calculateArea(): number;
  
  // 具体方法可以被继承或重写
  getColor(): string {
    return this.color;
  }
}

class Circle extends Shape {
  constructor(
    color: string,
    private radius: number
  ) {
    super(color);
  }
  
  // 实现抽象方法
  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
  
  // 添加特定方法
  getDiameter(): number {
    return this.radius * 2;
  }
}

class Rectangle extends Shape {
  constructor(
    color: string,
    private width: number,
    private height: number
  ) {
    super(color);
  }
  
  calculateArea(): number {
    return this.width * this.height;
  }
}

// const shape = new Shape("red"); // 错误:无法创建抽象类的实例
const circle = new Circle("blue", 5);
const rectangle = new Rectangle("green", 4, 6);

console.log(circle.calculateArea()); // 78.54...
console.log(rectangle.calculateArea()); // 24
console.log(circle.getColor()); // "blue"

2. 静态属性与方法

静态成员属于类本身而不是类的实例,无需创建实例即可访问。

2.1 静态属性与方法基础

typescript 复制代码
class MathUtils {
  // 静态属性
  static readonly PI = 3.14159;
  static count = 0;
  
  // 实例属性
  result: number = 0;
  
  constructor() {
    // 访问静态属性
    MathUtils.count++;
  }
  
  // 静态方法
  static square(num: number): number {
    return num * num;
  }
  
  // 实例方法可以访问静态成员
  calculateCircleArea(radius: number): number {
    this.result = MathUtils.PI * MathUtils.square(radius);
    return this.result;
  }
}

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.square(4)); // 16

const utils = new MathUtils();
console.log(utils.calculateCircleArea(5)); // 78.53975
console.log(MathUtils.count); // 1

2.2 设计模式中的应用

静态成员在许多设计模式中有重要应用,如工厂模式:

typescript 复制代码
// 产品接口
interface Product {
  name: string;
  use(): void;
}

// 具体产品
class ConcreteProductA implements Product {
  name = "Product A";
  
  use(): void {
    console.log("Using " + this.name);
  }
}

class ConcreteProductB implements Product {
  name = "Product B";
  
  use(): void {
    console.log("Using " + this.name);
  }
}

// 工厂类
class ProductFactory {
  // 静态工厂方法
  static createProduct(type: string): Product {
    switch (type) {
      case "A":
        return new ConcreteProductA();
      case "B":
        return new ConcreteProductB();
      default:
        throw new Error(`Product type ${type} not recognized.`);
    }
  }
}

// 使用工厂
const productA = ProductFactory.createProduct("A");
productA.use(); // "Using Product A"

2.3 单例模式实现

单例模式是静态成员的典型应用,确保一个类只有一个实例:

typescript 复制代码
class Database {
  private static instance: Database | null = null;
  private connectionString: string;
  
  // 私有构造函数,防止外部直接实例化
  private constructor(connectionString: string) {
    this.connectionString = connectionString;
    console.log(`Database initialized with: ${connectionString}`);
  }
  
  // 获取实例的静态方法
  public static getInstance(connectionString: string): Database {
    // 如果实例不存在,创建它
    if (!Database.instance) {
      Database.instance = new Database(connectionString);
    }
    return Database.instance;
  }
  
  public query(sql: string): void {
    console.log(`Executing query: ${sql} on ${this.connectionString}`);
  }
}

// 使用单例
const db1 = Database.getInstance("mysql://localhost:3306/mydb");
const db2 = Database.getInstance("mysql://localhost:3306/otherdb"); // 不会创建新实例

db1.query("SELECT * FROM users");
db2.query("SELECT * FROM products");

console.log(db1 === db2); // true,两个变量引用同一个实例

更现代的 TypeScript 单例实现:

typescript 复制代码
class ModernSingleton {
  private static instance: ModernSingleton;
  
  // 私有构造函数
  private constructor(public readonly id: string = Math.random().toString(36).substr(2, 9)) {
    console.log(`ModernSingleton initialized with ID: ${this.id}`);
  }
  
  // 使用访问器属性实现单例
  public static get Instance(): ModernSingleton {
    if (!ModernSingleton.instance) {
      ModernSingleton.instance = new ModernSingleton();
    }
    return ModernSingleton.instance;
  }
  
  public doSomething(): void {
    console.log(`Singleton ${this.id} doing something...`);
  }
}

// 使用
const instance1 = ModernSingleton.Instance;
const instance2 = ModernSingleton.Instance;

instance1.doSomething();
instance2.doSomething();

console.log(instance1.id === instance2.id); // true

3. 类实现接口

接口在 TypeScript 中扮演着核心角色,为类定义契约。

3.1 接口契约与实现

typescript 复制代码
// 定义接口
interface Vehicle {
  make: string;
  model: string;
  year: number;
  
  start(): void;
  stop(): void;
}

// 实现接口
class Car implements Vehicle {
  // 必须实现接口中定义的所有属性和方法
  make: string;
  model: string;
  year: number;
  
  // 可以添加接口未定义的其他成员
  private isRunning: boolean = false;
  
  constructor(make: string, model: string, year: number) {
    this.make = make;
    this.model = model;
    this.year = year;
  }
  
  start(): void {
    this.isRunning = true;
    console.log(`${this.make} ${this.model} started.`);
  }
  
  stop(): void {
    this.isRunning = false;
    console.log(`${this.make} ${this.model} stopped.`);
  }
  
  // 接口未定义的额外方法
  honk(): void {
    console.log("Beep beep!");
  }
}

// 使用实现了接口的类
const myCar: Vehicle = new Car("Toyota", "Corolla", 2022);
myCar.start(); // "Toyota Corolla started."
myCar.stop(); // "Toyota Corolla stopped."
// myCar.honk(); // 错误:Vehicle 接口中没有定义 honk 方法

// 使用完整的 Car 类型可以访问所有方法
const myCar2: Car = new Car("Honda", "Civic", 2023);
myCar2.honk(); // "Beep beep!"

多接口实现策略

TypeScript 允许一个类实现多个接口,这是一种强大的组合机制:

typescript 复制代码
// 定义多个接口
interface Printable {
  print(): void;
}

interface Serializable {
  serialize(): string;
}

interface Identifiable {
  id: string;
}

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

// 实现多个接口
class Document implements Printable, Serializable, Identifiable, Loggable {
  id: string;
  private content: string;
  private logEntries: string[] = [];
  
  constructor(id: string, content: string) {
    this.id = id;
    this.content = content;
  }
  
  // 实现 Printable
  print(): void {
    console.log(`Printing document: ${this.content}`);
    this.log("Document printed");
  }
  
  // 实现 Serializable
  serialize(): string {
    const data = JSON.stringify({
      id: this.id,
      content: this.content,
      timestamp: new Date()
    });
    this.log("Document serialized");
    return data;
  }
  
  // 实现 Loggable
  log(message: string): void {
    const timestamp = new Date().toISOString();
    this.logEntries.push(`[${timestamp}] ${message}`);
  }
  
  // 获取日志
  getLogs(): string[] {
    return this.logEntries;
  }
}

// 使用多接口实现类
const doc = new Document("doc-123", "TypeScript 接口示例");

// 可以作为任何接口类型使用
const printable: Printable = doc;
printable.print();

const serialized: string = (doc as Serializable).serialize();
console.log(serialized);

// 访问完整功能
console.log(`Document ID: ${doc.id}`);
console.log(`Logs: ${doc.getLogs().join("\n")}`);

3.3 接口组合与扩展

接口可以相互扩展,创建更复杂的类型关系:

typescript 复制代码
interface Entity {
  id: string;
  createdAt: Date;
}

interface Named {
  name: string;
}

// 扩展多个接口
interface Product extends Entity, Named {
  price: number;
  description: string;
}

// 实现扩展后的接口
class PhysicalProduct implements Product {
  id: string;
  createdAt: Date;
  name: string;
  price: number;
  description: string;
  weight: number; // 额外属性
  
  constructor(id: string, name: string, price: number, description: string, weight: number) {
    this.id = id;
    this.createdAt = new Date();
    this.name = name;
    this.price = price;
    this.description = description;
    this.weight = weight;
  }
  
  // 产品特定方法
  calculateShipping(): number {
    return this.weight * 0.5;
  }
}

const laptop = new PhysicalProduct(
  "prod-001",
  "Laptop Pro",
  1299,
  "High-performance laptop",
  2.5
);

console.log(`Shipping cost: $${laptop.calculateShipping()}`);

4. 装饰器模式

装饰器是 TypeScript 和 ES 规范中的一项实验性特性,允许以声明方式修改类、方法、属性和参数。

注意 :要使用装饰器,需要在 tsconfig.json 中启用 experimentalDecorators 选项。

4.1 类装饰器

类装饰器应用于类的构造函数,可以用来观察、修改或替换类定义:

typescript 复制代码
// 简单的类装饰器
function sealed(target: Function) {
  Object.seal(target);
  Object.seal(target.prototype);
  console.log(`${target.name} has been sealed`);
}

// 带参数的装饰器工厂
function component(name: string) {
  // 返回实际的装饰器函数
  return function(target: Function) {
    console.log(`Registering component: ${name}`);
    // 为类添加元数据
    Reflect.defineMetadata('componentName', name, target);
  };
}

// 应用装饰器
@sealed
@component('user-profile')
class UserProfile {
  constructor(private username: string) {}
  
  getProfile() {
    return `User: ${this.username}`;
  }
}

// 检索元数据(需要引入 reflect-metadata)
console.log(Reflect.getMetadata('componentName', UserProfile)); // 'user-profile'

4.2 方法与属性装饰器

typescript 复制代码
// 方法装饰器
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  // 保存原始方法
  const originalMethod = descriptor.value;
  
  // 替换方法
  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${propertyKey} with:`, args);
    // 调用原始方法并返回结果
    const result = originalMethod.apply(this, args);
    console.log(`Result of ${propertyKey}:`, result);
    return result;
  };
  
  return descriptor;
}

// 属性装饰器
function format(formatString: string) {
  return function(target: any, propertyKey: string) {
    // 属性值
    let value: any;
    
    // 创建 getter
    const getter = function() {
      return value;
    };
    
    // 创建 setter
    const setter = function(newValue: any) {
      if (typeof newValue === 'number' && formatString === 'currency') {
        value = `$${newValue.toFixed(2)}`;
      } else {
        value = newValue;
      }
    };
    
    // 替换属性
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class Order {
  @format('currency')
  price: number;
  
  constructor(price: number) {
    this.price = price;
  }
  
  @log
  calculateTotal(tax: number): number {
    const numericPrice = parseFloat(String(this.price).replace('$', ''));
    return numericPrice * (1 + tax);
  }
}

const order = new Order(29.99);
console.log(order.price); // "$29.99"
const total = order.calculateTotal(0.07); // 日志输出并返回总价

4.3 装饰器工厂函数

装饰器工厂是返回装饰器的函数,允许自定义装饰器行为:

typescript 复制代码
// 参数装饰器工厂
function required(target: Object, propertyKey: string, parameterIndex: number) {
  // 获取方法原始参数的元数据(需要 reflect-metadata)
  const existingRequiredParameters: number[] = Reflect.getOwnMetadata(
    'required',
    target,
    propertyKey
  ) || [];
  
  // 添加当前参数索引
  existingRequiredParameters.push(parameterIndex);
  
  // 更新元数据
  Reflect.defineMetadata(
    'required',
    existingRequiredParameters,
    target,
    propertyKey
  );
}

// 方法装饰器工厂,验证必填参数
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    // 获取必填参数列表
    const requiredParameters: number[] = Reflect.getOwnMetadata(
      'required',
      target,
      propertyKey
    ) || [];
    
    // 验证参数
    for (const index of requiredParameters) {
      if (index >= args.length || args[index] === undefined || args[index] === null) {
        throw new Error(`Parameter at index ${index} is required.`);
      }
    }
    
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

// 更复杂的装饰器组合
class API {
  @validate
  makeRequest(
    @required url: string,
    method: string = 'GET',
    @required data: object,
    headers?: object
  ) {
    console.log(`Making ${method} request to ${url} with data:`, data);
    // 实际请求逻辑...
    return { success: true };
  }
}

const api = new API();

// 正常工作
api.makeRequest('https://api.example.com', 'POST', { id: 123 });

// 抛出错误:url 为必填
// api.makeRequest(undefined, 'GET', { id: 123 });

// 抛出错误:data 为必填
// api.makeRequest('https://api.example.com', 'GET', undefined);

总结

随着 TypeScript 和 ECMAScript 标准的发展,面向对象特性将继续完善,为开发者提供更强大的工具来构建复杂的应用程序。使用这些特性时,关键是保持代码的简洁性和可读性,避免过度设计。

相关推荐
PBitW22 分钟前
为什么vite中TS报错,可以继续运行?Webpack不行?💡
前端·typescript
敖行客 Allthinker2 小时前
前端调试实战:DevTools调试工具 + 二分定位Bug法全总结
前端·typescript·chrome devtools
不瘦80斤不改名2 小时前
HalfCart:基于LBS的1\.5km本地拼单系统全栈实践与踩坑复盘
python·docker·typescript·pycharm
濮水大叔19 小时前
在大型 Vue 项目中,如何有效管理复杂的状态?“散装版” vs “套装版”
前端·vue.js·typescript
谷哥的小弟2 天前
TypeScript数组类型
javascript·typescript
退休倒计时2 天前
【每日一题】LeetCode 199. 二叉树的右视图 TypeScript
算法·leetcode·typescript
前端之虎陈随易3 天前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·vue.js·人工智能·typescript·node.js
后台开发者Ethan3 天前
类型分类、联合类型、交叉类型
typescript
鹰影473 天前
一款AI笔记助手和远程同步的markdown笔记idea-note
人工智能·笔记·rust·typescript·react
想你依然心痛3 天前
AtomCode 在前端开发中的实战体验:React + TypeScript 项目开发实录
前端·react.js·typescript