Skip to content

TypeScript 类

大白话解释: TypeScript 的类就像"蓝图"。比如"汽车蓝图"规定了汽车有轮子、发动机、方向盘,能跑、能停。类就是对象的蓝图,规定了对象有什么属性、能做什么操作。

为什么要用类?

  • 代码复用:写一次蓝图,可以创建无数个对象
  • 封装:把数据和操作打包在一起,外部不能随便改
  • 继承:子类可以继承父类的属性和方法,不用重复写
  • 多态:同一个方法,不同对象有不同的实现

TypeScript 提供了完整的面向对象编程支持,包括类、继承、访问修饰符、抽象类等特性。


类基础

基本类定义

ts
// ---- 基本类:定义对象的"蓝图" ----
// 大白话:就像"月饼模具",用模具可以做出很多月饼

class User {
  // 属性声明:定义对象有什么
  name: string
  age: number

  // 构造函数:创建对象时调用,用来初始化属性
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  // 方法:定义对象能做什么
  greet(): string {
    return `你好,我是${this.name},今年${this.age}岁`
  }
}

// 使用:用蓝图创建对象
const user = new User('张三', 25)
console.log(user.greet())  // 你好,我是张三,今年25岁
console.log(user.name)     // 张三
console.log(user.age)      // 25

简写构造函数

ts
// ---- 参数属性:在构造函数参数前加修饰符 ----
// 大白话:就像"快捷方式",不用手动写 this.xxx = xxx

class User {
  constructor(
    public name: string,      // 自动创建 public name 属性
    public age: number,       // 自动创建 public age 属性
    private email: string,    // 自动创建 private email 属性
    readonly id: number       // 自动创建 readonly id 属性
  ) {}

  greet(): string {
    return `你好,我是${this.name},今年${this.age}岁`
  }
}

// 等价于
class User {
  name: string
  age: number
  private email: string
  readonly id: number

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

// 使用
const user = new User('张三', 25, '[email protected]', 1)
console.log(user.name)  // ✅ 可以访问
// console.log(user.email)  // ❌ 错误,私有属性
// user.id = 2  // ❌ 错误,只读属性

访问修饰符

public

ts
// ---- public:公共属性,默认修饰符 ----
// 大白话:就像"公开信息",谁都能看、谁都能改

class User {
  public name: string
  public age: number

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

const user = new User('张三', 25)
console.log(user.name)  // ✅ 可以访问
console.log(user.age)   // ✅ 可以访问
user.name = '李四'       // ✅ 可以修改

private

ts
// ---- private:私有属性,只能在类内部访问 ----
// 大白话:就像"隐私信息",只有自己知道

class User {
  private password: string
  public name: string

  constructor(name: string, password: string) {
    this.name = name
    this.password = password
  }

  // 公共方法可以访问私有属性
  verifyPassword(password: string): boolean {
    return this.password === password
  }

  // 私有方法
  private encryptPassword(): string {
    return this.password.split('').reverse().join('')
  }
}

const user = new User('张三', '123456')
console.log(user.name)      // ✅ 可以访问
// console.log(user.password) // ❌ 错误,私有属性
console.log(user.verifyPassword('123456'))  // ✅ 通过公共方法访问
// user.encryptPassword()  // ❌ 错误,私有方法

protected

ts
// ---- protected:受保护属性,类内部和子类可以访问 ----
// 大白话:就像"家族秘密",只有自己和孩子知道

class Animal {
  protected name: string

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

  protected makeSound(): void {
    console.log('...')
  }
}

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

  bark(): void {
    console.log(`${this.name} 汪汪叫`)  // ✅ 子类可以访问
    this.makeSound()  // ✅ 子类可以访问
  }
}

const dog = new Dog('旺财')
dog.bark()  // ✅ 通过公共方法访问
// console.log(dog.name)  // ❌ 错误,受保护属性
// dog.makeSound()        // ❌ 错误,受保护方法

readonly

ts
// ---- readonly:只读属性,只能在声明时或构造函数中赋值 ----
// 大白话:就像"身份证号",一旦出生就定了,不能改

class User {
  readonly id: number
  name: string

  constructor(id: number, name: string) {
    this.id = id      // ✅ 构造函数中可以赋值
    this.name = name
  }

  updateName(name: string): void {
    this.name = name      // ✅ 可以修改
    // this.id = 2         // ❌ 错误,只读属性
  }
}

const user = new User(1, '张三')
user.name = '李四'  // ✅ 可以修改
// user.id = 2       // ❌ 错误,只读属性

继承

基本继承

ts
// ---- 继承:子类继承父类 ----
// 大白话:就像"遗传",孩子继承父母的基因

// 基类(父类)
class Animal {
  constructor(public name: string) {}

  makeSound(): void {
    console.log('...')
  }
}

// 继承(子类)
class Dog extends Animal {
  constructor(name: string) {
    super(name)  // 调用父类构造函数
  }

  // 重写方法:子类可以重新实现父类的方法
  makeSound(): void {
    console.log('汪汪叫')
  }

  // 新增方法:子类可以有自己的方法
  fetch(): void {
    console.log(`${this.name} 去捡球`)
  }
}

const dog = new Dog('旺财')
dog.makeSound()  // 汪汪叫(调用子类的方法)
dog.fetch()      // 旺财 去捡球
dog.name         // 旺财(继承自父类)

多层继承

ts
// ---- 多层继承:爷爷 → 爸爸 → 儿子 ----
// 大白话:就像"家族传承",一代传一代

class Animal {
  constructor(public name: string) {}
}

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

  breathe(): void {
    console.log(`${this.name} 在呼吸`)
  }
}

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

  bark(): void {
    console.log('汪汪叫')
  }
}

const dog = new Dog('旺财')
dog.breathe()  // 旺财 在呼吸(继承自 Mammal)
dog.bark()     // 汪汪叫(自己的方法)
dog.name       // 旺财(继承自 Animal)

抽象类

抽象类定义

ts
// ---- 抽象类:不能实例化,只能被继承 ----
// 大白话:就像"半成品蓝图",不能直接用,必须补充完整才能用

abstract class Shape {
  constructor(public color: string) {}

  // 抽象方法:子类必须实现(没有方法体)
  abstract getArea(): number

  // 普通方法:子类可以直接使用
  describe(): string {
    return `这是一个${this.color}的图形,面积为${this.getArea()}`
  }
}

// 具体类:实现抽象方法
class Circle extends Shape {
  constructor(color: string, public radius: number) {
    super(color)
  }

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

class Rectangle extends Shape {
  constructor(color: string, public width: number, public height: number) {
    super(color)
  }

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

// 使用
const circle = new Circle('红色', 5)
console.log(circle.describe())  // 这是一个红色的图形,面积为78.53981633974483

// const shape = new Shape('蓝色')  // ❌ 错误,抽象类不能实例化

抽象属性

ts
// ---- 抽象属性:子类必须实现的属性 ----
// 大白话:就像"必填项",子类必须提供这些属性

abstract class Animal {
  abstract name: string
  abstract readonly legs: number

  abstract makeSound(): void

  describe(): string {
    return `${this.name} 有 ${this.legs} 条腿`
  }
}

class Dog extends Animal {
  name = '狗'      // ✅ 实现抽象属性
  legs = 4         // ✅ 实现抽象属性

  makeSound(): void {
    console.log('汪汪叫')
  }
}

const dog = new Dog()
console.log(dog.describe())  // 狗 有 4 条腿

静态成员

静态属性和方法

ts
// ---- 静态成员:属于类本身,而不是实例 ----
// 大白话:就像"公司制度",属于公司,不属于某个员工

class MathUtils {
  // 静态属性
  static PI = 3.14159

  // 静态方法
  static add(a: number, b: number): number {
    return a + b
  }

  static multiply(a: number, b: number): number {
    return a * b
  }
}

// 使用:直接通过类名访问
console.log(MathUtils.PI)          // 3.14159
console.log(MathUtils.add(1, 2))   // 3
console.log(MathUtils.multiply(2, 3))  // 6

// const math = new MathUtils()
// console.log(math.PI)  // ❌ 错误,静态成员不能通过实例访问

静态单例模式

ts
// ---- 单例模式:确保一个类只有一个实例 ----
// 大白话:就像"地球",只有一个,大家都用同一个

class Database {
  private static instance: Database

  private constructor(public host: string, public port: number) {}

  static getInstance(): Database {
    if (!Database.instance) {
      Database.instance = new Database('localhost', 3306)
    }
    return Database.instance
  }

  connect(): void {
    console.log(`连接到 ${this.host}:${this.port}`)
  }
}

// 使用
const db1 = Database.getInstance()
const db2 = Database.getInstance()
console.log(db1 === db2)  // true,是同一个实例

db1.connect()  // 连接到 localhost:3306

接口实现

类实现接口

ts
// ---- 接口实现:类必须实现接口定义的所有方法 ----
// 大白话:就像"合同",签了合同就必须按合同办事

// 接口
interface Printable {
  print(): void
}

interface Serializable {
  serialize(): string
}

// 类实现多个接口
class Document implements Printable, Serializable {
  constructor(public title: string, public content: string) {}

  print(): void {
    console.log(`打印文档:${this.title}`)
    console.log(this.content)
  }

  serialize(): string {
    return JSON.stringify({ title: this.title, content: this.content })
  }
}

// 使用
const doc = new Document('TypeScript 教程', '内容...')
doc.print()
console.log(doc.serialize())

接口约束构造函数

ts
// ---- 接口约束构造函数 ----
// 大白话:就像"工厂必须能生产产品"

interface Constructor<T, A extends any[] = any[]> {
  new (...args: A): T
}

function createInstance<T, A extends any[]>(ctor: Constructor<T, A>, ...args: A): T {
  return new ctor(...args)
}

class User {
  constructor(public name: string) {}
}

const user = createInstance(User, '张三')
console.log(user.name)  // 张三

方法装饰器

注意: 装饰器有两种语法:

  • 新语法(TC39 Stage 3):TypeScript 5.0+ 默认支持,参数是 (value, context)
  • 旧语法(experimental):需要在 tsconfig.json 中开启 "experimentalDecorators": true

下面示例使用旧语法(更常见),如果使用新语法,参数格式不同。

方法装饰器基础

ts
// ---- 方法装饰器:给方法添加额外功能 ----
// 大白话:就像"手机壳",不改变手机本身,但给手机加了新功能
// 旧语法需要在 tsconfig.json 开启:{ "compilerOptions": { "experimentalDecorators": true } }

// 方法装饰器(旧语法)
// 参数说明:
// target: 类的原型对象(不是实例)
// propertyKey: 方法名(如 'add')
// descriptor: 属性描述符,包含原方法
function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  // 保存原方法
  const originalMethod = descriptor.value

  // 替换原方法,加上日志功能
  descriptor.value = function (...args: any[]) {
    console.log(`调用 ${propertyKey},参数:`, args)
    // apply(this, args):调用原方法,this 绑定到当前实例
    const result = originalMethod.apply(this, args)
    console.log(`${propertyKey} 返回:`, result)
    return result
  }

  return descriptor
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b
  }

  @Log
  multiply(a: number, b: number): number {
    return a * b
  }
}

const calc = new Calculator()
calc.add(1, 2)
// 调用 add,参数: [1, 2]
// add 返回: 3

访问器装饰器

ts
// ---- 访问器装饰器:给 getter/setter 添加额外功能 ----
// 大白话:就像"门卫",在 getter/setter 前后加检查

function Enumerable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = value
  }
}

class User {
  private _name: string

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

  @Enumerable(true)
  get name(): string {
    return this._name
  }

  @Enumerable(false)
  get displayName(): string {
    return `用户:${this._name}`
  }
}

混入(Mixins)

混入模式

ts
// ---- 混入:给类添加额外功能 ----
// 大白话:就像"贴膜",给手机贴上膜,手机就多了防刮功能
// 为什么用混入?TypeScript 只支持单继承,混入可以在不修改原类的情况下添加功能

// Constructor<T>:一个构造函数类型,T 是实例类型
// new (...args: any[]) => T:表示"能 new 出 T 类型实例的构造函数"
type Constructor<T = {}> = new (...args: any[]) => T

// Timestamped 混入:给任何类添加时间戳功能
// TBase extends Constructor:TBase 必须是一个构造函数
// 返回值:一个继承自 TBase 的新类,增加了 createdAt、updatedAt 属性和 touch 方法
function Timestamped<TBase extends Constructor>(Base: TBase) {
  // extends Base:创建一个继承自 Base 的匿名类
  return class extends Base {
    createdAt = new Date()      // 创建时间
    updatedAt = new Date()      // 更新时间

    touch() {
      this.updatedAt = new Date()  // 更新时间戳
    }
  }
}

// Activatable 混入:给任何类添加激活/停用功能
function Activatable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    isActive = false

    activate() {
      this.isActive = true
    }

    deactivate() {
      this.isActive = false
    }
  }
}

// 使用混入
class User {
  constructor(public name: string) {}
}

// TimestampedUser:User + 时间戳功能
const TimestampedUser = Timestamped(User)

// ActivatableUser:User + 激活/停用功能
const ActivatableUser = Activatable(User)

// 链式混入:先加 Activatable,再加 Timestamped
// 等价于 Timestamped(Activatable(User))
const TimestampedActivatableUser = Timestamped(Activatable(User))

// 使用
const user = new TimestampedActivatableUser('张三')
console.log(user.createdAt)  // Date(来自 Timestamped)
user.activate()               // 来自 Activatable
console.log(user.isActive)   // true
user.touch()                  // 来自 Timestamped
console.log(user.updatedAt)  // Date

实际应用示例

服务类

ts
// ---- API 服务类 ----
// 大白话:就像"快递公司",负责发送和接收请求

class ApiService {
  private baseUrl: string
  private headers: Record<string, string>

  constructor(baseUrl: string, token?: string) {
    this.baseUrl = baseUrl
    this.headers = {
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
    }
  }

  async get<T>(endpoint: string): Promise<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      headers: this.headers,
    })
    return response.json()
  }

  async post<T>(endpoint: string, data: any): Promise<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify(data),
    })
    return response.json()
  }

  async put<T>(endpoint: string, data: any): Promise<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: 'PUT',
      headers: this.headers,
      body: JSON.stringify(data),
    })
    return response.json()
  }

  async delete<T>(endpoint: string): Promise<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: 'DELETE',
      headers: this.headers,
    })
    return response.json()
  }
}

// 使用
const api = new ApiService('https://api.example.com', 'token123')
const users = await api.get<User[]>('/users')
const newUser = await api.post<User>('/users', { name: '张三' })

事件发射器

ts
// ---- 事件发射器类 ----
// 大白话:就像"广播站",可以发布消息,也可以订阅消息

class EventEmitter<T extends Record<string, any>> {
  private listeners: Map<keyof T, Set<Function>> = new Map()

  on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set())
    }
    this.listeners.get(event)!.add(listener)
  }

  off<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
    this.listeners.get(event)?.delete(listener)
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    this.listeners.get(event)?.forEach(listener => listener(data))
  }
}

// 使用
interface UserEvents {
  login: { userId: string; timestamp: Date }
  logout: { userId: string }
  error: { message: string; code: number }
}

const emitter = new EventEmitter<UserEvents>()

emitter.on('login', (data) => {
  console.log(`用户 ${data.userId} 登录`)
})

emitter.on('error', (data) => {
  console.log(`错误 ${data.code}:${data.message}`)
})

emitter.emit('login', { userId: '123', timestamp: new Date() })

通用仓储模式

ts
// ---- 通用仓储模式 ----
// 大白话:就像"仓库管理员",负责管理所有货物的进出

// Entity 接口:所有实体必须有 id
interface Entity {
  id: number
}

// T extends Entity:T 必须有 id 属性
class Repository<T extends Entity> {
  private items: T[] = []
  private nextId = 1

  // Omit<T, 'id'>:从 T 中排除 id 属性
  // 为什么用 Omit?因为新增时 id 由系统自动生成,不需要用户传
  // 例如:User 有 id, name, email,add 时只需要传 { name, email }
  add(item: Omit<T, 'id'>): T {
    // as T:告诉 TS 合并后的对象是 T 类型
    const newItem = { ...item, id: this.nextId++ } as T
    this.items.push(newItem)
    return newItem
  }

  findById(id: number): T | undefined {
    // find 返回 T | undefined(可能找不到)
    return this.items.find(item => item.id === id)
  }

  findAll(): T[] {
    // 返回副本,防止外部直接修改内部数据
    return [...this.items]
  }

  // Partial<T>:T 的所有属性变为可选
  // 为什么用 Partial?因为更新时只需要传要改的字段
  // 例如:update(1, { name: '李四' }) 只改 name,不改 email
  update(id: number, updates: Partial<T>): T | undefined {
    const index = this.items.findIndex(item => item.id === id)
    if (index === -1) return undefined
    // 展开运算符合并:保留原数据,覆盖新数据
    this.items[index] = { ...this.items[index], ...updates }
    return this.items[index]
  }

  delete(id: number): boolean {
    const index = this.items.findIndex(item => item.id === id)
    if (index === -1) return false
    this.items.splice(index, 1)
    return true
  }
}

// 使用
interface User extends Entity {
  name: string
  email: string
}

const userRepo = new Repository<User>()
// add 的参数是 Omit<User, 'id'> = { name: string; email: string },不需要传 id
const user = userRepo.add({ name: '张三', email: '[email protected]' })
const found = userRepo.findById(1)
// update 的参数是 Partial<User> = { name?: string; email?: string },可以只传部分
userRepo.update(1, { name: '李四' })
userRepo.delete(1)

常见问题

类型断言 vs 类型守卫

ts
// 类型断言(不安全)
const user = {} as User

// 类型守卫(安全)
function isUser(obj: any): obj is User {
  return obj && typeof obj.name === 'string' && typeof obj.age === 'number'
}

访问修饰符选择

ts
// 选择指南:
// - public:默认,任何地方都可以访问
// - private:只在类内部使用,外部不能访问
// - protected:类内部和子类可以访问
// - readonly:只能在声明时或构造函数中赋值

抽象类 vs 接口

ts
// 抽象类:
// - 可以有实现代码
// - 可以有构造函数
// - 可以有访问修饰符
// - 只能单继承

// 接口:
// - 只能定义结构,不能有实现
// - 不能有构造函数
// - 所有成员天生公开(不支持访问修饰符)
// - 可以多实现

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准