TypeScript 深度强化第三天:类与继承、装饰器、模块系统 🏗️
学会搭建大型"积木建筑" - 掌握 TypeScript 的面向对象编程核心
📖 今日学习目标
第三天我们将深入 TypeScript 的核心架构特性,这些是构建大型应用程序的基石:
- 🏗️ 类与继承 - 面向对象编程的核心,代码复用和组织的利器
- 📦 模块系统 - 代码组织与重用,构建可维护的项目结构
- ✨ 装饰器 - 元编程的利器,横切关注点的优雅解决方案
- 🔗 声明合并 - 类型系统的灵活性扩展
- 🎯 枚举 - 常量管理的最佳实践
📚 目录
1. TypeScript 类系统深度解析 🏗️
1.1 为什么需要类?
在现代软件开发中,面向对象编程(OOP)已经成为主流的编程范式之一。TypeScript 的类系统继承了 JavaScript ES6+ 的 class 语法,并在此基础上添加了强大的类型检查、访问修饰符等特性,让我们能够构建更加健壮和可维护的应用程序。
面向对象编程的核心思想:
面向对象编程是一种程序设计思想,它将现实世界中的事物抽象为程序中的对象。每个对象都有自己的属性(数据)和方法(行为)。这种编程方式更接近人类的思维模式,让复杂的程序变得更容易理解和维护。
类的核心优势:
- 📊 封装性(Encapsulation) - 将数据和操作数据的方法组合在一起,隐藏内部实现细节
- 🔄 继承性(Inheritance) - 子类可以继承父类的属性和方法,实现代码复用
- 🎭 多态性(Polymorphism) - 同一个接口可以有不同的实现方式
- 🔒 访问控制 - 通过访问修饰符控制成员的可见性和访问权限
TypeScript 类系统的特殊优势:
- 编译时类型检查 - 在开发阶段就能发现类型错误
- 丰富的访问修饰符 - public、private、protected、readonly 等
- 接口实现 - 确保类遵循特定的契约
- 抽象类支持 - 为相关类提供共同的基础结构
- 装饰器支持 - 提供元编程能力
1.2 基础类定义与实例化
什么是类?
类(Class)是面向对象编程的核心概念,它是创建对象的模板或蓝图。就像建筑师用图纸来建造房子一样,我们用类来创建对象。类定义了对象应该具有的属性和方法,但它本身不是对象,只有通过实例化才能创建具体的对象。
类与对象的关系:
- 类(Class):抽象的模板,定义了对象的结构和行为
- 对象(Object):类的具体实例,拥有实际的数据和功能
- 实例化(Instantiation):根据类创建对象的过程
类的核心组成要素:
- 属性(Properties):存储对象的状态数据
- 方法(Methods):定义对象的行为和功能
- 构造函数(Constructor):初始化新创建的对象
- 访问修饰符(Access Modifiers):控制成员的访问权限
让我们通过一个用户管理系统的例子来深入理解这些概念:
// 用户类 - 展示类的基本结构
class User {
// 公共属性 - 外部可以直接访问和修改
public username: string
public email: string
// 私有属性 - 只有类内部可以访问,外部无法直接访问
private password: string
// 只读属性 - 一旦设置就不能修改,保证数据的不变性
readonly userId: number
// 静态属性 - 属于整个类,而不是某个特定实例
static totalUsers: number = 0
// 构造函数 - 创建对象时自动调用,用于初始化对象
constructor(username: string, email: string, password: string) {
this.username = username
this.email = email
this.password = password
this.userId = Date.now() // 使用时间戳作为唯一ID
User.totalUsers++ // 每创建一个用户,总数加1
}
// 公共方法 - 外部可以调用的功能
public getUserInfo(): string {
return `用户:${this.username},邮箱:${this.email}`
}
// 私有方法 - 只在类内部使用,封装内部逻辑
private validatePassword(inputPassword: string): boolean {
return this.password === inputPassword
}
// 业务方法 - 组合使用公共和私有方法
public login(password: string): boolean {
if (this.validatePassword(password)) {
console.log(`${this.username} 登录成功!`)
return true
}
console.log(`密码错误!`)
return false
}
// 静态方法 - 通过类名直接调用,不需要实例化
static getTotalUsers(): number {
return User.totalUsers
}
}
类的使用示例:
理解了类的定义后,让我们看看如何使用这个类:
// 创建用户实例 - 实例化过程
const user1 = new User('张三', 'zhangsan@example.com', '123456')
const user2 = new User('李四', 'lisi@example.com', '654321')
// 访问公共属性
console.log(user1.username) // "张三"
console.log(user1.email) // "zhangsan@example.com"
// 调用公共方法
console.log(user1.getUserInfo()) // "用户:张三,邮箱:zhangsan@example.com"
// 测试登录功能
user1.login('123456') // "张三 登录成功!"
user1.login('wrong') // "密码错误!"
// 访问静态属性和方法
console.log(User.totalUsers) // 2
console.log(User.getTotalUsers()) // 2
// 尝试访问私有属性(会报错)
// console.log(user1.password) // ❌ 编译错误:属性"password"为私有属性
关键理解点:
- 封装性的体现:
private属性如password外部无法直接访问,保护了敏感数据的安全性 - 数据完整性:
readonly属性如userId一旦设置就不能修改,确保数据的一致性 - 类级别数据:
static属性如totalUsers属于整个类,所有实例共享同一份数据 - 方法的层次性:公共方法对外提供服务,私有方法封装内部逻辑
1.3 继承与多态深入理解
继承的本质和价值
继承(Inheritance)是面向对象编程的核心特性之一,它允许我们基于现有的类创建新的类。这就像生物学中的遗传一样,子类会"遗传"父类的特征,同时还可以发展出自己独特的特征。
继承解决的核心问题:
- 代码重复 - 避免在多个类中编写相同的代码
- 维护困难 - 当需要修改共同功能时,只需修改父类
- 扩展性差 - 新增相似功能的类时,可以基于现有类进行扩展
- 概念不清晰 - 通过继承层次表达对象间的关系
多态的深层含义
多态(Polymorphism)意味着"多种形态"。在编程中,它指的是同一个方法在不同的类中可以有不同的实现。这就像"运动"这个概念,鱼类的运动是游泳,鸟类的运动是飞翔,人类的运动是跑步,虽然都是运动,但实现方式完全不同。
让我们通过一个完整的员工管理系统来深入理解继承和多态:
// 员工基类 - 定义所有员工的共同特征
class Employee {
// 受保护属性 - 子类可以访问,但外部不能直接访问
protected name: string
protected department: string
protected baseSalary: number
protected hireDate: Date
constructor(name: string, department: string, baseSalary: number) {
this.name = name
this.department = department
this.baseSalary = baseSalary
this.hireDate = new Date()
}
// 基础工资计算方法 - 子类可以重写(多态的基础)
public calculateSalary(): number {
return this.baseSalary
}
// 获取员工基本信息
public getInfo(): string {
return `员工:${this.name},部门:${this.department}`
}
// 计算工作年限
public getWorkYears(): number {
const currentDate = new Date()
return currentDate.getFullYear() - this.hireDate.getFullYear()
}
}
现在让我们创建不同类型的员工类,展示继承和多态的威力:
// 销售员工类 - 继承自Employee,有自己特殊的工资计算方式
class SalesEmployee extends Employee {
private salesAmount: number = 0
private commissionRate: number
constructor(name: string, baseSalary: number, commissionRate: number) {
// 调用父类构造函数,继承父类的初始化逻辑
super(name, '销售部', baseSalary)
this.commissionRate = commissionRate
}
// 重写父类的工资计算方法(多态的体现)
public calculateSalary(): number {
const baseSalary = super.calculateSalary() // 调用父类方法
const commission = this.salesAmount * this.commissionRate
return baseSalary + commission
}
// 销售员工特有的方法
public recordSale(amount: number): void {
this.salesAmount += amount
console.log(`${this.name} 新增销售业绩:${amount}元,总业绩:${this.salesAmount}元`)
}
// 获取销售业绩
public getSalesAmount(): number {
return this.salesAmount
}
}
// 技术员工类 - 另一种继承实现
class TechEmployee extends Employee {
private techLevel: number
private projects: string[] = []
constructor(name: string, baseSalary: number, techLevel: number) {
super(name, '技术部', baseSalary)
this.techLevel = techLevel
}
// 重写工资计算方法 - 基于技术等级
public calculateSalary(): number {
const baseSalary = super.calculateSalary()
const techBonus = this.techLevel * 1000 // 每个技术等级1000元奖金
const projectBonus = this.projects.length * 500 // 每个项目500元奖金
return baseSalary + techBonus + projectBonus
}
// 技术员工特有的方法
public assignProject(projectName: string): void {
this.projects.push(projectName)
console.log(`${this.name} 被分配到项目:${projectName}`)
}
public getProjects(): string[] {
return [...this.projects] // 返回副本,保护内部数据
}
}
// 管理员工类 - 展示更复杂的继承关系
class ManagerEmployee extends Employee {
private teamMembers: Employee[] = []
private managementBonus: number
constructor(name: string, baseSalary: number, managementBonus: number) {
super(name, '管理部', baseSalary)
this.managementBonus = managementBonus
}
// 管理者的工资计算包含管理奖金和团队奖金
public calculateSalary(): number {
const baseSalary = super.calculateSalary()
const teamBonus = this.teamMembers.length * 200 // 每个下属200元奖金
return baseSalary + this.managementBonus + teamBonus
}
// 管理者特有的方法
public addTeamMember(employee: Employee): void {
this.teamMembers.push(employee)
console.log(`${employee.getInfo()} 加入了 ${this.name} 的团队`)
}
public getTeamSize(): number {
return this.teamMembers.length
}
}
多态的实际应用演示:
// 创建不同类型的员工
const employees: Employee[] = [
new Employee('张行政', '行政部', 4000),
new SalesEmployee('李销售', 5000, 0.1),
new TechEmployee('王程序', 8000, 3),
new ManagerEmployee('赵经理', 12000, 3000)
]
// 为销售员工添加业绩
const salesEmployee = employees[1] as SalesEmployee
salesEmployee.recordSale(50000)
// 为技术员工分配项目
const techEmployee = employees[2] as TechEmployee
techEmployee.assignProject('用户管理系统')
techEmployee.assignProject('数据分析平台')
// 为管理者添加团队成员
const manager = employees[3] as ManagerEmployee
manager.addTeamMember(employees[0])
manager.addTeamMember(employees[1])
// 多态的威力:同样的方法调用,不同的实现结果
console.log('\n=== 员工工资计算(多态演示)===')
employees.forEach((employee, index) => {
console.log(`${index + 1}. ${employee.getInfo()}`)
console.log(` 工资:${employee.calculateSalary()}元`)
console.log(` 工作年限:${employee.getWorkYears()}年`)
console.log('---')
})
继承和多态的核心价值:
- 代码复用:所有员工类都继承了基础的
name、department、getInfo()等成员 - 方法重写:每种员工都有自己独特的工资计算方式,体现了多态性
- 统一接口:虽然实现不同,但都可以调用相同的方法名,简化了使用
- 扩展性强:可以轻松添加新的员工类型,如实习生、顾问等
- 维护性好:修改共同功能时只需修改基类
设计原则体现:
- 开闭原则:对扩展开放,对修改关闭
- 里氏替换原则:子类可以替换父类使用
- 依赖倒置原则:依赖抽象而不是具体实现
1.4 抽象类与接口实现的深度剖析
抽象类(Abstract Class)的设计哲学
抽象类是面向对象设计中的一个重要概念,它介于普通类和接口之间。抽象类的设计哲学是:定义一个不完整的类,为子类提供共同的基础结构和部分实现,同时强制子类实现某些关键方法。
抽象类的核心特点:
- 不能直接实例化 - 它是一个不完整的类,必须通过子类来使用
- 可以包含抽象方法 - 只有声明没有实现,强制子类实现
- 可以包含具体方法 - 提供公共功能,子类可以直接使用
- 可以有构造函数 - 为子类提供初始化逻辑
- 可以有属性 - 定义子类的公共数据结构
接口(Interface)的设计理念
接口定义了一个契约或协议,它规定了实现该接口的类必须具备哪些能力。接口的设计理念是:关注行为而不是实现,定义"能做什么"而不是"怎么做"。
接口的核心特点:
- 纯粹的契约 - 只定义结构,不包含任何实现
- 多重实现 - 一个类可以实现多个接口
- 接口继承 - 接口可以继承其他接口
- 声明合并 - 同名接口会自动合并
让我们通过一个图形绘制系统来深入理解抽象类和接口的应用:
// 抽象类 - 图形基类
// 为所有图形提供共同的基础结构和部分实现
abstract class Shape {
protected name: string
protected color: string = '默认色'
protected position: { x: number; y: number } = { x: 0, y: 0 }
constructor(name: string) {
this.name = name
}
// 抽象方法 - 每种图形计算面积和周长的方式不同,必须由子类实现
abstract calculateArea(): number
abstract calculatePerimeter(): number
// 具体方法 - 所有图形都有的通用功能,子类可以直接使用
public getDescription(): string {
const area = this.calculateArea().toFixed(2)
const perimeter = this.calculatePerimeter().toFixed(2)
return `这是一个${this.color}的${this.name},面积:${area},周长:${perimeter}`
}
// 设置颜色 - 通用功能
public setColor(color: string): void {
this.color = color
console.log(`${this.name}颜色已设置为${color}`)
}
// 设置位置 - 通用功能
public setPosition(x: number, y: number): void {
this.position = { x, y }
console.log(`${this.name}位置已设置为(${x}, ${y})`)
}
// 获取基本信息
public getInfo(): string {
return `图形:${this.name},颜色:${this.color},位置:(${this.position.x}, ${this.position.y})`
}
}
现在让我们实现具体的图形类:
// 圆形类 - 实现抽象类
class Circle extends Shape {
private radius: number
constructor(radius: number) {
super('圆形') // 调用抽象类的构造函数
this.radius = radius
}
// 必须实现的抽象方法 - 圆形面积计算
public calculateArea(): number {
return Math.PI * this.radius * this.radius
}
// 必须实现的抽象方法 - 圆形周长计算
public calculatePerimeter(): number {
return 2 * Math.PI * this.radius
}
// 圆形特有的方法
public getRadius(): number {
return this.radius
}
public setRadius(radius: number): void {
if (radius <= 0) {
throw new Error('半径必须大于0')
}
this.radius = radius
console.log(`圆形半径已设置为${radius}`)
}
}
// 矩形类 - 另一个抽象类实现
class Rectangle extends Shape {
private width: number
private height: number
constructor(width: number, height: number) {
super('矩形')
this.width = width
this.height = height
}
// 实现抽象方法
public calculateArea(): number {
return this.width * this.height
}
public calculatePerimeter(): number {
return 2 * (this.width + this.height)
}
// 矩形特有的方法
public getDimensions(): { width: number; height: number } {
return { width: this.width, height: this.height }
}
public resize(width: number, height: number): void {
this.width = width
this.height = height
console.log(`矩形尺寸已调整为${width}x${height}`)
}
}
现在让我们定义一些接口,展示接口的强大能力:
// 绘制能力接口
interface Drawable {
draw(): void
setColor(color: string): void
}
// 移动能力接口
interface Movable {
moveTo(x: number, y: number): void
getPosition(): { x: number; y: number }
}
// 缩放能力接口
interface Resizable {
resize(scale: number): void
getScale(): number
}
// 动画能力接口
interface Animatable {
startAnimation(): void
stopAnimation(): void
isAnimating(): boolean
}
现在让我们创建一个实现多个接口的高级图形类:
// 交互式圆形 - 继承抽象类并实现多个接口
class InteractiveCircle extends Circle implements Drawable, Movable, Resizable, Animatable {
private scale: number = 1
private isAnimationRunning: boolean = false
private animationTimer?: NodeJS.Timeout
constructor(radius: number, color: string = '蓝色') {
super(radius)
this.setColor(color)
}
// 实现 Drawable 接口
public draw(): void {
const pos = this.getPosition()
const actualRadius = this.getRadius() * this.scale
console.log(`在坐标(${pos.x}, ${pos.y})绘制一个${this.color}的圆形,半径:${actualRadius.toFixed(2)}`)
}
// 实现 Movable 接口
public moveTo(x: number, y: number): void {
const currentPos = this.getPosition()
console.log(`圆形从(${currentPos.x}, ${currentPos.y})移动到(${x}, ${y})`)
this.setPosition(x, y)
}
public getPosition(): { x: number; y: number } {
return { ...this.position } // 返回副本,保护内部数据
}
// 实现 Resizable 接口
public resize(scale: number): void {
if (scale <= 0) {
throw new Error('缩放比例必须大于0')
}
console.log(`圆形从缩放比例${this.scale}调整到${scale}`)
this.scale = scale
}
public getScale(): number {
return this.scale
}
// 实现 Animatable 接口
public startAnimation(): void {
if (this.isAnimationRunning) {
console.log('动画已在运行中')
return
}
this.isAnimationRunning = true
console.log('开始动画效果')
let angle = 0
this.animationTimer = setInterval(() => {
angle += 0.1
const x = Math.cos(angle) * 50
const y = Math.sin(angle) * 50
this.setPosition(x, y)
this.draw()
}, 100)
}
public stopAnimation(): void {
if (!this.isAnimationRunning) {
console.log('动画未在运行')
return
}
this.isAnimationRunning = false
if (this.animationTimer) {
clearInterval(this.animationTimer)
this.animationTimer = undefined
}
console.log('动画已停止')
}
public isAnimating(): boolean {
return this.isAnimationRunning
}
// 组合功能的高级方法
public performComplexAction(): void {
console.log('=== 执行复杂动作序列 ===')
this.draw()
this.moveTo(10, 10)
this.resize(1.5)
this.setColor('红色')
this.draw()
this.startAnimation()
// 3秒后停止动画
setTimeout(() => {
this.stopAnimation()
console.log('=== 复杂动作序列完成 ===')
}, 3000)
}
}
使用示例和多态演示:
console.log('=== 抽象类使用示例 ===')
// const shape = new Shape("测试"); // ❌ 错误!不能直接实例化抽象类
// 创建具体的图形实例
const circle = new Circle(5)
circle.setColor('绿色')
console.log(circle.getDescription())
const rectangle = new Rectangle(4, 6)
rectangle.setColor('紫色')
console.log(rectangle.getDescription())
console.log('\n=== 接口实现示例 ===')
const interactiveCircle = new InteractiveCircle(3, '橙色')
interactiveCircle.performComplexAction()
console.log('\n=== 多态演示(抽象类) ===')
const shapes: Shape[] = [
new Circle(4),
new Rectangle(3, 5),
new InteractiveCircle(2, '青色')
]
shapes.forEach((shape, index) => {
console.log(`图形${index + 1}: ${shape.getDescription()}`)
})
console.log('\n=== 接口多态演示 ===')
// 将实现了相同接口的对象放在一起使用
const drawableObjects: Drawable[] = [
new InteractiveCircle(2, '红色'),
new InteractiveCircle(3, '蓝色')
]
drawableObjects.forEach(obj => {
obj.draw() // 多态调用
})
抽象类 vs 接口的选择指南:
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 实例化 | 不能直接实例化 | 不能实例化 |
| 方法实现 | 可以有具体方法和抽象方法 | 只能有方法签名 |
| 属性 | 可以有实例属性 | 只能有属性签名 |
| 构造函数 | 可以有构造函数 | 不能有构造函数 |
| 继承数量 | 单继承 | 多实现 |
| 扩展性 | 通过继承扩展 | 通过实现和继承扩展 |
| 使用场景 | 有共同属性和部分共同行为 | 定义能力契约 |
设计建议:
-
使用抽象类的场景:
- 多个类有共同的属性和部分共同的方法实现
- 需要为子类提供默认实现
- 希望强制子类实现某些关键方法
-
使用接口的场景:
- 定义类必须具备的能力
- 需要多重继承的效果
- 希望实现松耦合的设计
-
组合使用:
- 抽象类提供基础结构和共同实现
- 接口定义额外的能力和契约
- 通过继承抽象类和实现接口获得最大的灵活性
通过这个详细的例子,我们可以看到抽象类和接口如何协同工作,为我们提供了强大而灵活的面向对象设计能力。
1.5 成员可见性修饰符详解
访问修饰符的设计意义
访问修饰符是面向对象编程中实现封装性的重要工具。它们帮助我们控制类成员的可见性,从而保护内部实现细节,防止外部代码的不当访问和修改。这种设计遵循了"最小权限原则"——只暴露必要的接口,隐藏实现细节。
TypeScript 提供的四种访问修饰符:
- public(公共):任何地方都可以访问,这是默认的访问级别
- private(私有):只能在定义它的类内部访问
- protected(受保护):可以在类及其子类中访问
- readonly(只读):只能在声明时或构造函数中赋值
访问修饰符在实际开发中的重要性:
在大型软件项目中,访问修饰符不仅仅是语法特性,更是架构设计的重要工具。它们帮助我们:
- 建立清晰的API边界 - 明确哪些是对外接口,哪些是内部实现
- 防止意外的依赖关系 - 避免外部代码依赖不稳定的内部实现
- 提高代码的可维护性 - 内部实现的修改不会影响外部使用者
- 增强代码的安全性 - 保护敏感数据和关键逻辑不被误用
让我们通过一个银行账户系统来深入理解访问修饰符的实际应用:
// 银行账户类 - 展示不同访问修饰符的使用
class BankAccount {
// 公共属性 - 外部可以访问的基本信息
public accountHolder: string
public accountType: string
// 受保护属性 - 子类可以访问,但外部不能直接访问
protected balance: number
protected interestRate: number
// 私有属性 - 只有本类内部可以访问的敏感信息
private accountNumber: string
private pin: string
private transactionHistory: Transaction[]
// 只读属性 - 一旦设置就不能修改的重要信息
readonly bankCode: string
readonly creationDate: Date
constructor(
accountHolder: string,
accountType: string,
initialBalance: number,
pin: string
) {
this.accountHolder = accountHolder
this.accountType = accountType
this.balance = initialBalance
this.pin = pin
this.interestRate = 0.02 // 默认2%年利率
this.accountNumber = this.generateAccountNumber()
this.transactionHistory = []
this.bankCode = 'BANK001'
this.creationDate = new Date()
}
// 公共方法 - 对外提供的服务接口
public getAccountInfo(): string {
return `账户持有人: ${this.accountHolder}, 账户类型: ${this.accountType}`
}
public checkBalance(inputPin: string): number | null {
if (this.validatePin(inputPin)) {
return this.balance
}
console.log('PIN码错误,无法查询余额')
return null
}
public deposit(amount: number, pin: string): boolean {
if (!this.validatePin(pin)) {
console.log('PIN码错误,存款失败')
return false
}
if (amount <= 0) {
console.log('存款金额必须大于0')
return false
}
this.balance += amount
this.recordTransaction('存款', amount)
console.log(`存款成功,当前余额: ${this.balance}`)
return true
}
public withdraw(amount: number, pin: string): boolean {
if (!this.validatePin(pin)) {
console.log('PIN码错误,取款失败')
return false
}
if (amount <= 0) {
console.log('取款金额必须大于0')
return false
}
if (amount > this.balance) {
console.log('余额不足,取款失败')
return false
}
this.balance -= amount
this.recordTransaction('取款', amount)
console.log(`取款成功,当前余额: ${this.balance}`)
return true
}
// 受保护方法 - 子类可以使用的工具方法
protected calculateInterest(): number {
return this.balance * this.interestRate
}
protected updateBalance(newBalance: number): void {
this.balance = newBalance
this.recordTransaction('余额更新', newBalance)
}
// 私有方法 - 只有本类内部使用的实现细节
private validatePin(inputPin: string): boolean {
return this.pin === inputPin
}
private generateAccountNumber(): string {
return 'ACC' + Date.now().toString().slice(-8)
}
private recordTransaction(type: string, amount: number): void {
const transaction: Transaction = {
id: Date.now().toString(),
type,
amount,
timestamp: new Date(),
balance: this.balance
}
this.transactionHistory.push(transaction)
}
}
// 交易记录接口
interface Transaction {
id: string
type: string
amount: number
timestamp: Date
balance: number
}
现在让我们创建一个储蓄账户类,展示继承中访问修饰符的作用:
// 储蓄账户类 - 继承银行账户
class SavingsAccount extends BankAccount {
private minimumBalance: number
constructor(
accountHolder: string,
initialBalance: number,
pin: string,
minimumBalance: number = 100
) {
super(accountHolder, '储蓄账户', initialBalance, pin)
this.minimumBalance = minimumBalance
// ✅ 可以访问受保护属性
this.interestRate = 0.035 // 储蓄账户利率更高
}
// 重写取款方法,添加最低余额限制
public withdraw(amount: number, pin: string): boolean {
// ✅ 可以访问受保护属性进行检查
if (this.balance - amount < this.minimumBalance) {
console.log(`取款失败:余额不能低于最低限额 ${this.minimumBalance}`)
return false
}
// 调用父类的取款方法
return super.withdraw(amount, pin)
}
// 储蓄账户特有的功能
public applyInterest(pin: string): boolean {
if (!this.validatePin(pin)) { // ❌ 编译错误:validatePin是私有方法
console.log('PIN码错误,无法应用利息')
return false
}
// ✅ 可以使用受保护方法
const interest = this.calculateInterest()
const newBalance = this.balance + interest
// ✅ 可以使用受保护方法
this.updateBalance(newBalance)
console.log(`利息已应用,新余额: ${newBalance}`)
return true
}
// 正确的利息应用方法
public applyInterestCorrect(pin: string): boolean {
// 通过公共方法验证PIN
const currentBalance = this.checkBalance(pin)
if (currentBalance === null) {
return false
}
const interest = this.calculateInterest()
const newBalance = this.balance + interest
this.updateBalance(newBalance)
console.log(`利息已应用,新余额: ${newBalance}`)
return true
}
}
使用示例和访问控制演示:
// 创建银行账户
const account = new BankAccount('张三', '活期账户', 1000, '1234')
const savings = new SavingsAccount('李四', 5000, '5678', 500)
// ✅ 可以访问公共成员
console.log(account.accountHolder) // "张三"
console.log(account.getAccountInfo()) // "账户持有人: 张三, 账户类型: 活期账户"
console.log(account.bankCode) // "BANK001" (只读属性)
// ✅ 可以调用公共方法
account.deposit(500, '1234')
account.withdraw(200, '1234')
console.log(account.checkBalance('1234')) // 1300
// ❌ 不能访问受保护成员
// console.log(account.balance) // 编译错误:属性"balance"受保护
// account.calculateInterest() // 编译错误:方法"calculateInterest"受保护
// ❌ 不能访问私有成员
// console.log(account.accountNumber) // 编译错误:属性"accountNumber"为私有
// account.validatePin('1234') // 编译错误:方法"validatePin"为私有
// ❌ 不能修改只读属性
// account.bankCode = "NEW001" // 编译错误:无法分配到"bankCode",因为它是只读属性
// 储蓄账户的特殊功能
savings.applyInterestCorrect('5678')
savings.withdraw(1000, '5678') // 会检查最低余额限制
访问修饰符的设计原则和最佳实践:
-
最小权限原则:
- 默认使用最严格的访问级别(private)
- 只有在需要时才放宽权限
- 避免过度暴露内部实现
-
接口设计原则:
- 公共方法应该提供清晰、稳定的API
- 避免暴露可能变化的内部细节
- 使用受保护成员为继承体系提供扩展点
-
数据保护原则:
- 敏感数据(如密码、账号)使用private
- 业务数据(如余额)使用protected,允许子类访问
- 配置数据(如利率)可以是protected或private
-
只读属性的使用:
- 用于不应该改变的重要标识(如账号、创建时间)
- 提供数据完整性保证
- 防止意外的数据修改
访问修饰符的实际价值总结:
| 修饰符 | 访问范围 | 使用场景 | 设计价值 |
|---|---|---|---|
| public | 任何地方 | 对外API、配置属性 | 开放接口,服务外部 |
| protected | 类和子类 | 继承体系内共享的数据和方法 | 受控共享,支持扩展 |
| private | 仅当前类 | 内部实现、敏感数据 | 完全封装,保护实现 |
| readonly | 只读访问 | 不可变的重要数据 | 数据完整性,防止意外修改 |
通过合理使用访问修饰符,我们可以构建出既安全又灵活的类层次结构,这是面向对象设计的核心技能之一。
1.6 静态成员与实例成员的深入理解
静态成员 vs 实例成员的本质区别
在面向对象编程中,理解静态成员和实例成员的区别是非常重要的。这个区别不仅仅是语法上的,更是设计思想上的:
- 实例成员:属于每个具体的对象实例,每个实例都有自己独立的一份数据
- 静态成员:属于整个类,所有实例共享同一份数据,甚至不需要创建实例就可以使用
静态成员的使用场景:
- 工具函数 - 不依赖实例状态的纯函数
- 全局配置 - 整个类的配置参数
- 计数器 - 统计类的实例数量
- 缓存 - 类级别的数据缓存
- 工厂方法 - 创建实例的静态方法
让我们通过一个数学工具类来深入理解静态成员和实例成员的应用:
// 数学工具类 - 展示静态成员和实例成员的配合使用
class MathCalculator {
// 静态属性 - 属于整个类,所有实例共享
static readonly PI: number = 3.14159265359
static readonly E: number = 2.71828182846
static calculationCount: number = 0 // 统计总计算次数
// 实例属性 - 属于每个具体的计算器实例
public precision: number
public history: string[] = []
private instanceId: string
constructor(precision: number = 2) {
this.precision = precision
this.instanceId = `calc_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`
MathCalculator.calculationCount++ // 每创建一个实例,总数加1
}
// 静态方法 - 通过类名调用,不能访问实例成员
static add(a: number, b: number): number {
MathCalculator.calculationCount++ // ✅ 可以访问静态成员
// this.precision // ❌ 不能访问实例成员,因为没有this上下文
return a + b
}
static multiply(a: number, b: number): number {
MathCalculator.calculationCount++
return a * b
}
// 静态工具方法
static isEven(num: number): boolean {
return num % 2 === 0
}
static factorial(n: number): number {
MathCalculator.calculationCount++
if (n <= 1) return 1
return n * MathCalculator.factorial(n - 1)
}
// 静态工厂方法 - 创建特定配置的实例
static createHighPrecisionCalculator(): MathCalculator {
return new MathCalculator(10)
}
static createBasicCalculator(): MathCalculator {
return new MathCalculator(2)
}
// 实例方法 - 通过实例调用,可以访问所有成员
public formatNumber(num: number): string {
MathCalculator.calculationCount++ // ✅ 可以访问静态成员
const formatted = num.toFixed(this.precision) // ✅ 可以访问实例成员
this.addToHistory(`格式化数字: ${num} -> ${formatted}`) // ✅ 可以访问实例方法
return formatted
}
public calculateCircleArea(radius: number): string {
const area = MathCalculator.PI * radius * radius // 使用静态常量
const result = this.formatNumber(area) // 使用实例方法格式化
this.addToHistory(`圆形面积计算: 半径=${radius}, 面积=${result}`)
return result
}
public power(base: number, exponent: number): string {
const result = Math.pow(base, exponent)
const formatted = this.formatNumber(result)
this.addToHistory(`幂运算: ${base}^${exponent} = ${formatted}`)
return formatted
}
// 实例方法 - 管理计算历史
private addToHistory(operation: string): void {
this.history.push(`[${this.instanceId}] ${operation}`)
}
public getHistory(): string[] {
return [...this.history] // 返回副本,保护内部数据
}
public clearHistory(): void {
this.history = []
console.log(`计算器 ${this.instanceId} 的历史记录已清空`)
}
// 实例方法 - 获取实例信息
public getInstanceInfo(): string {
return `计算器ID: ${this.instanceId}, 精度: ${this.precision}, 历史记录: ${this.history.length}条`
}
// 静态方法 - 获取类级别的统计信息
static getGlobalStats(): string {
return `总计算次数: ${MathCalculator.calculationCount}`
}
// 静态方法 - 重置全局计数器
static resetGlobalCounter(): void {
MathCalculator.calculationCount = 0
console.log('全局计算计数器已重置')
}
}
使用示例和对比演示:
console.log('=== 静态成员使用示例 ===')
// 使用静态常量 - 不需要创建实例
console.log('圆周率:', MathCalculator.PI)
console.log('自然常数:', MathCalculator.E)
// 使用静态方法 - 通过类名直接调用
console.log('2 + 3 =', MathCalculator.add(2, 3))
console.log('4 * 5 =', MathCalculator.multiply(4, 5))
console.log('6是偶数吗?', MathCalculator.isEven(6))
console.log('5的阶乘:', MathCalculator.factorial(5))
// 查看全局统计
console.log(MathCalculator.getGlobalStats()) // "总计算次数: 5"
console.log('\n=== 实例成员使用示例 ===')
// 创建不同精度的计算器实例
const basicCalc = new MathCalculator(2)
const preciseCalc = new MathCalculator(6)
// 使用工厂方法创建实例
const highPrecCalc = MathCalculator.createHighPrecisionCalculator()
// 每个实例有自己独立的状态
console.log('基础计算器信息:', basicCalc.getInstanceInfo())
console.log('精确计算器信息:', preciseCalc.getInstanceInfo())
console.log('高精度计算器信息:', highPrecCalc.getInstanceInfo())
// 实例方法调用
console.log('基础计算器 - 圆面积:', basicCalc.calculateCircleArea(5))
console.log('精确计算器 - 圆面积:', preciseCalc.calculateCircleArea(5))
console.log('高精度计算器 - 2^10:', highPrecCalc.power(2, 10))
// 查看各自的历史记录
console.log('\n基础计算器历史:')
basicCalc.getHistory().forEach(record => console.log(' ', record))
console.log('\n精确计算器历史:')
preciseCalc.getHistory().forEach(record => console.log(' ', record))
// 全局统计会反映所有计算
console.log('\n' + MathCalculator.getGlobalStats())
console.log('\n=== 静态与实例成员的对比 ===')
// 静态成员:所有实例共享
console.log('创建实例前的计算次数:', MathCalculator.calculationCount)
const calc1 = new MathCalculator(3)
const calc2 = new MathCalculator(4)
console.log('创建2个实例后的计算次数:', MathCalculator.calculationCount)
// 实例成员:每个实例独立
calc1.calculateCircleArea(3)
calc2.calculateCircleArea(4)
console.log('calc1的历史记录数:', calc1.getHistory().length)
console.log('calc2的历史记录数:', calc2.getHistory().length)
console.log('全局计算次数:', MathCalculator.calculationCount)
静态成员和实例成员的设计原则:
-
静态成员适用场景:
- 工具函数:不依赖实例状态的纯函数
- 常量:类相关的不变值(如PI、配置参数)
- 全局状态:需要在所有实例间共享的数据
- 工厂方法:创建实例的便捷方法
-
实例成员适用场景:
- 实例状态:每个对象独有的数据
- 行为方法:依赖实例状态的操作
- 配置属性:每个实例可以不同的设置
-
设计建议:
- 优先考虑实例成员,除非确实需要类级别的功能
- 静态方法不应该访问实例成员
- 使用静态工厂方法提供创建实例的便捷方式
- 静态属性常用于存储常量和全局配置
内存和性能考虑:
| 特性 | 静态成员 | 实例成员 |
|---|---|---|
| 内存占用 | 类加载时创建一份 | 每个实例一份 |
| 访问速度 | 稍快(无需实例化) | 正常 |
| 生命周期 | 程序运行期间一直存在 | 随实例创建和销毁 |
| 适用场景 | 工具方法、常量、全局状态 | 实例数据、行为方法 |
通过合理使用静态成员和实例成员,我们可以设计出既高效又清晰的类结构,这是面向对象设计的重要技能。
2. 模块系统与命名空间 📦
2.1 ES6 模块系统的核心理念
什么是模块?为什么需要模块化?
在软件开发的早期,所有代码都写在一个文件中,随着项目规模的增长,这种做法变得越来越难以维护。模块化编程的出现解决了这个问题。模块(Module)是代码组织的基本单元,它将相关的功能封装在一起,对外提供清晰的接口。
模块化编程的历史演进:
- 无模块时代:所有代码在全局作用域,命名冲突严重
- IIFE时代:使用立即执行函数创建局部作用域
- CommonJS/AMD时代:Node.js和浏览器的模块化尝试
- ES6模块时代:语言原生支持,成为标准
模块系统解决的核心问题:
- 命名空间污染:避免全局变量冲突
- 依赖管理:明确模块间的依赖关系
- 代码复用:一次编写,多处使用
- 按需加载:只加载需要的功能,优化性能
- 团队协作:不同开发者可以独立开发不同模块
- 代码维护:模块化的代码更容易理解和修改
TypeScript 模块系统的特殊优势:
TypeScript 完全支持 ES6 模块语法,并在此基础上提供了额外的类型安全保障:
- 编译时类型检查:在编译阶段就能发现模块间的类型错误
- 智能提示:IDE 可以提供精确的代码补全和重构支持
- 接口导出:可以导出类型定义,实现类型的复用
- 声明文件:为 JavaScript 库提供类型定义
让我们通过一个实际的例子来理解模块的基本使用:
// 文件:utils/math.ts - 数学工具模块
// 常量导出
export const PI = 3.14159265359
export const E = 2.71828182846
// 具名函数导出
export function add(a: number, b: number): number {
return a + b
}
export function multiply(a: number, b: number): number {
return a * b
}
// 默认导出 - 每个模块只能有一个
export default function subtract(a: number, b: number): number {
return a - b
}
导入的多种方式和使用场景:
// 1. 具名导入 - 明确指定需要的功能
import { add, multiply, PI } from './utils/math'
// 2. 重命名导入 - 避免命名冲突
import { add as sum, multiply as product } from './utils/math'
// 3. 默认导入 - 可以任意命名
import subtract from './utils/math'
import minus from './utils/math' // 同样有效
// 4. 命名空间导入 - 导入所有导出内容
import * as MathUtils from './utils/math'
// 5. 混合导入 - 同时导入默认和具名导出
import subtract, { add, PI } from './utils/math'
不同导入方式的使用示例:
// 使用具名导入
console.log(add(2, 3)) // 5
console.log(multiply(4, 5)) // 20
// 使用重命名导入
console.log(sum(2, 3)) // 5
console.log(product(4, 5)) // 20
// 使用默认导入
console.log(subtract(10, 3)) // 7
// 使用命名空间导入
console.log(MathUtils.add(2, 3)) // 5
console.log(MathUtils.PI) // 3.14159265359
console.log(MathUtils.default(10, 3)) // 7 (默认导出)
模块导出的设计原则:
-
具名导出适用场景:
- 工具函数集合
- 常量和配置
- 多个相关的类或接口
- 可以导出多个,导入时必须使用确切的名称
-
默认导出适用场景:
- 模块的主要功能或类
- 单一职责的模块
- 每个模块只能有一个,导入时可以任意命名
-
混合导出策略:
- 主要功能使用默认导出
- 辅助功能和配置使用具名导出
- 提供最大的使用灵活性
模块系统的核心价值体现:
通过这个简单的数学工具模块例子,我们可以看到模块系统的价值:
- 清晰的边界:每个模块有明确的功能范围
- 可控的接口:只暴露需要的功能,隐藏实现细节
- 灵活的使用:使用者可以按需导入,避免不必要的依赖
- 类型安全:TypeScript 确保导入导出的类型匹配
2.2 高级模块特性
重新导出(Re-export)
重新导出让我们可以创建统一的入口点,简化导入路径:
// 文件:utils/string.ts
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
export function reverse(str: string): string {
return str.split('').reverse().join('')
}
// 文件:utils/number.ts
export function isEven(num: number): boolean {
return num % 2 === 0
}
export function random(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min
}
// 文件:utils/index.ts - 统一入口
export * from './string' // 重新导出所有string工具
export * from './number' // 重新导出所有number工具
// 也可以选择性重新导出
export { capitalize as cap } from './string'
使用重新导出:
// 现在可以从一个地方导入所有工具
import { capitalize, reverse, isEven, random } from './utils'
console.log(capitalize('hello')) // "Hello"
console.log(isEven(4)) // true
动态导入(Dynamic Import)
动态导入允许我们按需加载模块,这对于代码分割和懒加载很有用:
// 条件导入
async function loadUtilsIfNeeded() {
if (someCondition) {
const utils = await import('./utils')
return utils.capitalize('hello')
}
}
// 动态导入在函数中使用
async function processData(data: any[]) {
if (data.length > 1000) {
// 只有在处理大量数据时才加载重型工具
const { heavyProcessor } = await import('./heavy-utils')
return heavyProcessor(data)
}
return data
}
2.3 模块解析策略
TypeScript 支持两种模块解析策略:
- Classic - 传统解析策略(已弃用)
- Node - 模仿 Node.js 的解析策略(推荐)
相对导入 vs 非相对导入:
// 相对导入 - 相对于当前文件的路径
import { User } from './models/User' // 同级目录
import { Config } from '../config/app' // 上级目录
import { Utils } from '../../shared/utils' // 上两级目录
// 非相对导入 - 从 node_modules 或基础路径解析
import { lodash } from 'lodash' // 第三方库
import { ApiService } from '@/services/api' // 使用路径映射
路径映射配置(tsconfig.json):
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["*"], // @/components -> src/components
"@models/*": ["models/*"], // @models/User -> src/models/User
"@utils/*": ["shared/utils/*"] // @utils/math -> src/shared/utils/math
}
}
}
2.4 命名空间详解
什么是命名空间?
命名空间是TypeScript特有的代码组织方式,主要用于全局代码的组织和避免命名冲突。
命名空间的特点:
- 全局作用域:在全局范围内创建逻辑分组
- 嵌套支持:可以创建层次化的命名空间结构
- 声明合并:同名命名空间会自动合并
- 编译输出:编译为 IIFE(立即执行函数表达式)
// 基础命名空间
namespace Geometry {
export interface Point {
x: number
y: number
}
export class Circle {
constructor(
public center: Point,
public radius: number
) {}
public area(): number {
return Math.PI * this.radius * this.radius
}
}
export function distance(p1: Point, p2: Point): number {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2))
}
// 嵌套命名空间
export namespace Utils {
export function createPoint(x: number, y: number): Point {
return { x, y }
}
export function createCircle(x: number, y: number, radius: number): Circle {
return new Circle(createPoint(x, y), radius)
}
}
}
// 命名空间合并
namespace Geometry {
export class Rectangle {
constructor(
public width: number,
public height: number
) {}
public area(): number {
return this.width * this.height
}
}
}
使用命名空间:
// 使用命名空间中的类型和函数
const point: Geometry.Point = { x: 0, y: 0 }
const circle = new Geometry.Circle(point, 5)
const rect = new Geometry.Rectangle(10, 20)
console.log(circle.area()) // 78.54
console.log(rect.area()) // 200
// 使用嵌套命名空间
const newCircle = Geometry.Utils.createCircle(10, 10, 3)
const distance = Geometry.distance(point, { x: 3, y: 4 })
命名空间 vs 模块的选择指南:
| 特性 | 命名空间 | 模块 |
|---|---|---|
| 作用域 | 全局 | 文件级 |
| 加载方式 | 立即加载 | 按需加载 |
| 工具支持 | 有限 | 完整 |
| 代码分割 | 不支持 | 支持 |
| 推荐场景 | 全局工具库 | 现代应用开发 |
现代开发建议:
- 新项目优先使用模块系统
- 命名空间主要用于全局类型声明和库的类型定义
- 避免在模块化项目中混用命名空间
3. 装饰器与元编程 ✨
3.1 装饰器的本质与设计哲学
什么是装饰器?为什么需要装饰器?
装饰器(Decorator)是一种设计模式,也是 TypeScript 中的一个实验性特性。它允许我们在不修改原有代码的情况下,为类、方法、属性或参数添加额外的功能。装饰器的核心思想来源于"装饰者模式",这是面向对象设计中的一个重要模式。
装饰器解决的核心问题:
在传统的面向对象编程中,我们经常遇到这样的问题:
- 横切关注点:日志记录、性能监控、权限验证等功能需要在多个地方重复实现
- 代码耦合:业务逻辑与辅助功能混合在一起,难以维护
- 功能扩展:为现有类添加新功能时,需要修改原有代码
- 代码重复:相同的辅助功能在多个类中重复编写
装饰器的设计哲学:
装饰器遵循几个重要的设计原则:
- 开闭原则:对扩展开放,对修改关闭
- 单一职责原则:每个装饰器只关注一个特定的功能
- 依赖倒置原则:依赖抽象而不是具体实现
- 组合优于继承:通过组合多个装饰器实现复杂功能
生活中的装饰器类比:
为了更好地理解装饰器,我们可以用生活中的例子来类比:
-
咖啡店的咖啡制作:
- 基础咖啡(原始对象)
- 加糖装饰器:为咖啡添加甜味
- 加奶装饰器:为咖啡添加奶香
- 加冰装饰器:将咖啡变成冰饮
- 最终结果:冰糖奶咖啡
-
智能手机的功能扩展:
- 基础手机(原始对象)
- 保护壳装饰器:增加防摔功能
- 钢化膜装饰器:增加屏幕保护
- 支架装饰器:增加观看便利性
- 最终结果:功能完备的防护手机
-
汽车的改装升级:
- 基础汽车(原始对象)
- 导航装饰器:增加导航功能
- 行车记录仪装饰器:增加安全记录
- 音响装饰器:提升音响效果
- 最终结果:功能丰富的智能汽车
装饰器在软件开发中的价值:
- 功能增强:在不修改原有代码的基础上添加新功能
- 代码复用:同一个装饰器可以应用到多个不同的目标上
- 关注点分离:将业务逻辑与辅助功能分离,提高代码的可读性和可维护性
- 动态组合:可以根据需要动态组合不同的装饰器,实现灵活的功能配置
- 元编程能力:在编译时或运行时修改程序的行为
TypeScript 装饰器的特殊性:
TypeScript 的装饰器是实验性特性,但它们提供了强大的元编程能力:
- 编译时处理:装饰器在编译时就会被处理,可以进行代码转换
- 类型安全:结合 TypeScript 的类型系统,提供类型安全的装饰器
- 反射支持:配合 reflect-metadata 库,可以获取丰富的类型信息
- 多种目标:可以装饰类、方法、属性、参数等不同的目标
装饰器的分类和应用场景:
-
类装饰器:修改或扩展类的定义
- 单例模式实现
- 依赖注入
- 类的元数据添加
-
方法装饰器:增强方法的功能
- 日志记录
- 性能监控
- 缓存机制
- 权限验证
-
属性装饰器:控制属性的行为
- 数据验证
- 序列化控制
- 响应式数据
-
参数装饰器:处理方法参数
- 参数验证
- 依赖注入
- 数据转换
启用装饰器的配置:
在使用 TypeScript 装饰器之前,需要在 tsconfig.json 中启用相关选项:
{
"compilerOptions": {
"experimentalDecorators": true, // 启用装饰器支持
"emitDecoratorMetadata": true, // 启用装饰器元数据
"target": "ES2015", // 目标版本
"lib": ["ES2015", "DOM"] // 包含的库
}
}
装饰器的执行时机和原理:
装饰器在 TypeScript 编译过程中会被转换为普通的 JavaScript 代码。理解装饰器的执行时机对于正确使用装饰器非常重要:
- 编译时执行:装饰器函数在类定义时就会执行
- 从下到上:多个装饰器的执行顺序是从下到上
- 参数传递:装饰器会接收不同的参数,用于获取目标对象的信息
- 返回值处理:装饰器的返回值会影响最终的结果
通过深入理解装饰器的设计哲学和应用场景,我们可以更好地利用这个强大的工具来构建灵活、可维护的代码结构。
3.2 方法装饰器的深入应用
方法装饰器的核心作用与设计思想
方法装饰器是装饰器体系中最常用也最实用的一种,它专门用于增强方法的功能。在企业级应用开发中,我们经常需要为方法添加各种横切关注点(Cross-cutting Concerns),比如:
- 性能监控:记录方法执行时间,识别性能瓶颈
- 日志记录:自动记录方法的调用情况和参数
- 缓存机制:为计算密集型方法添加结果缓存
- 错误处理:统一的异常处理和重试机制
- 权限验证:在方法执行前检查用户权限
- 数据验证:自动验证方法参数的合法性
方法装饰器的技术原理
方法装饰器接收三个参数:
target:对于静态方法是类的构造函数,对于实例方法是类的原型对象propertyKey:方法的名称descriptor:方法的属性描述符,包含方法的实现
装饰器通过修改 descriptor.value 来替换原始方法,从而实现功能增强。这种方式遵循了代理模式(Proxy Pattern)的设计思想。
实际应用场景分析
让我们通过几个实际的业务场景来理解方法装饰器的价值:
- 性能优化场景:在电商系统中,商品推荐算法可能需要大量计算,我们需要监控其性能表现
- 缓存优化场景:数据库查询结果可以缓存,避免重复查询
- 可靠性保障场景:网络请求可能失败,需要自动重试机制
方法装饰器用于监视、修改或替换方法定义。
// 执行时间装饰器
function measureTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
const start = performance.now()
const result = originalMethod.apply(this, args)
const end = performance.now()
console.log(`${propertyKey} 执行时间: ${(end - start).toFixed(2)}ms`)
return result
}
return descriptor
}
// 缓存装饰器
function cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
const cacheMap = new Map()
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args)
if (cacheMap.has(key)) {
console.log(`缓存命中: ${propertyKey}`)
return cacheMap.get(key)
}
const result = originalMethod.apply(this, args)
cacheMap.set(key, result)
console.log(`缓存存储: ${propertyKey}`)
return result
}
return descriptor
}
// 重试装饰器
function retry(maxAttempts: number = 3) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
let lastError: any
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await originalMethod.apply(this, args)
} catch (error) {
lastError = error
console.log(`${propertyKey} 第${attempt}次尝试失败:`, error.message)
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 1000 * attempt))
}
}
}
throw lastError
}
return descriptor
}
}
// 使用装饰器的类
class DataProcessor {
@measureTime
@cache
fibonacci(n: number): number {
if (n <= 1) return n
return this.fibonacci(n - 1) + this.fibonacci(n - 2)
}
@retry(3)
async fetchData(url: string): Promise<any> {
// 模拟可能失败的网络请求
if (Math.random() < 0.7) {
throw new Error('网络请求失败')
}
return { data: 'success', url }
}
}
3.3 类装饰器的企业级应用
类装饰器的设计价值与应用场景
类装饰器是作用于整个类的装饰器,它可以监视、修改或完全替换类的定义。在大型企业应用中,类装饰器常用于实现一些架构级的功能模式:
主要应用场景:
- 设计模式实现:单例模式、工厂模式等
- 依赖注入:自动注入依赖的服务或配置
- 元数据管理:为类添加元数据信息,用于框架处理
- AOP切面编程:为类的所有方法添加统一的切面逻辑
- ORM映射:数据库实体类的自动映射配置
- API路由注册:Web框架中的路由自动注册
类装饰器的技术特点:
- 接收一个参数:类的构造函数
- 可以返回新的构造函数来替换原始类
- 在类定义时执行,而不是实例化时
- 可以修改类的原型,为所有实例添加共同功能
企业开发中的实际价值: 类装饰器帮助我们实现"配置即代码"的理念,通过简单的装饰器声明就能为类添加复杂的功能,大大减少了样板代码,提高了开发效率。
类装饰器用于监视、修改或替换类定义。
// 单例装饰器
function singleton<T extends { new (...args: any[]): {} }>(constructor: T) {
let instance: any = null
return class extends constructor {
constructor(...args: any[]) {
if (instance) {
return instance
}
super(...args)
instance = this
}
}
}
// 日志装饰器(记录类的创建)
function logged(className: string) {
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
console.log(`创建${className}实例,参数:`, args)
super(...args)
}
}
}
}
// 自动绑定装饰器(绑定所有方法的this)
function autobind<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(...args)
// 自动绑定所有方法
const proto = Object.getPrototypeOf(this)
Object.getOwnPropertyNames(proto).forEach(name => {
if (name !== 'constructor' && typeof this[name] === 'function') {
this[name] = this[name].bind(this)
}
})
}
}
}
// 使用类装饰器
@singleton
@logged('配置管理器')
@autobind
class ConfigManager {
private config: Record<string, any> = {}
set(key: string, value: any): void {
this.config[key] = value
console.log(`设置配置: ${key} = ${value}`)
}
get(key: string): any {
return this.config[key]
}
getHandler() {
// 返回绑定了this的方法,可以安全地传递给其他函数
return this.get
}
}
3.4 属性装饰器的精细化控制
属性装饰器的设计理念与应用价值
属性装饰器专注于对类属性的精细化控制,它虽然不能直接修改属性的值,但可以通过元数据或属性描述符的方式来影响属性的行为。在现代Web开发中,属性装饰器常用于:
核心应用场景:
- 数据验证:为属性添加验证规则,确保数据的合法性
- 序列化控制:控制属性在JSON序列化时的行为
- 响应式数据:实现数据变化的自动监听和响应
- ORM字段映射:数据库字段与对象属性的映射关系
- API文档生成:为属性添加描述信息,自动生成API文档
- 权限控制:控制属性的读写权限
属性装饰器的技术特点:
- 接收两个参数:目标对象和属性名
- 主要用于添加元数据,而不是修改属性值
- 通常与其他装饰器或框架配合使用
- 在属性定义时执行,为后续处理做准备
与其他装饰器的协同作用: 属性装饰器往往不是独立工作的,它通常与方法装饰器、类装饰器配合,形成完整的功能体系。比如在数据验证场景中,属性装饰器定义验证规则,方法装饰器执行验证逻辑。
属性装饰器用于监视属性的声明。
// 只读装饰器
function readonly(target: any, propertyKey: string) {
let value: any
Object.defineProperty(target, propertyKey, {
get() {
return value
},
set(newValue: any) {
if (value !== undefined) {
throw new Error(`属性 ${propertyKey} 是只读的`)
}
value = newValue
},
enumerable: true,
configurable: true
})
}
// 验证装饰器
function validate(validator: (value: any) => boolean, message: string) {
return function (target: any, propertyKey: string) {
let value: any
Object.defineProperty(target, propertyKey, {
get() {
return value
},
set(newValue: any) {
if (!validator(newValue)) {
throw new Error(`${propertyKey}: ${message}`)
}
value = newValue
},
enumerable: true,
configurable: true
})
}
}
// 格式化装饰器
function format(formatter: (value: any) => any) {
return function (target: any, propertyKey: string) {
let value: any
Object.defineProperty(target, propertyKey, {
get() {
return value
},
set(newValue: any) {
value = formatter(newValue)
},
enumerable: true,
configurable: true
})
}
}
// 使用属性装饰器
class User {
@readonly
id: number = Date.now()
@validate(val => typeof val === 'string' && val.length > 0, '用户名不能为空')
@format(val => val.trim().toLowerCase())
username: string = ''
@validate(val => /^[\w-]+@[\w-]+\.\w+$/.test(val), '邮箱格式不正确')
email: string = ''
@validate(val => val >= 18, '年龄必须大于等于18岁')
age: number = 0
}
3.5 参数装饰器
参数装饰器用于监视参数的声明。
// 参数验证装饰器
function validateParam(validator: (value: any) => boolean, message: string) {
return function (target: any, propertyKey: string, parameterIndex: number) {
const existingValidators = Reflect.getMetadata('validators', target, propertyKey) || []
existingValidators.push({ index: parameterIndex, validator, message })
Reflect.defineMetadata('validators', existingValidators, target, propertyKey)
}
}
// 方法装饰器:执行参数验证
function validateParams(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
const validators = Reflect.getMetadata('validators', target, propertyKey) || []
descriptor.value = function (...args: any[]) {
for (const { index, validator, message } of validators) {
if (!validator(args[index])) {
throw new Error(`参数${index + 1}: ${message}`)
}
}
return originalMethod.apply(this, args)
}
return descriptor
}
// 使用参数装饰器
class MathService {
@validateParams
divide(
@validateParam(val => typeof val === 'number', '必须是数字') a: number,
@validateParam(val => typeof val === 'number' && val !== 0, '除数不能为0') b: number
): number {
return a / b
}
@validateParams
power(
@validateParam(val => typeof val === 'number' && val >= 0, '底数必须是非负数') base: number,
@validateParam(val => typeof val === 'number' && val >= 0, '指数必须是非负数') exponent: number
): number {
return Math.pow(base, exponent)
}
}
3.6 装饰器工厂和组合
装饰器工厂是返回装饰器的函数,可以接受配置参数:
// 装饰器工厂示例
function debounce(delay: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
let timeoutId: NodeJS.Timeout
descriptor.value = function (...args: any[]) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
originalMethod.apply(this, args)
}, delay)
}
return descriptor
}
}
function throttle(limit: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
let inThrottle = false
descriptor.value = function (...args: any[]) {
if (!inThrottle) {
originalMethod.apply(this, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
return descriptor
}
}
// 装饰器组合使用
class SearchComponent {
@debounce(300)
onSearchInput(query: string): void {
console.log(`搜索: ${query}`)
}
@throttle(1000)
onScroll(): void {
console.log('页面滚动')
}
@measureTime
@cache
@retry(2)
async searchData(query: string): Promise<any[]> {
// 模拟搜索逻辑
return []
}
}
装饰器的执行顺序:
装饰器从下到上执行(就像洋葱模型):
class Example {
@first
@second
@third
method() {}
}
// 执行顺序:third -> second -> first
通过这些装饰器示例,我们可以看到装饰器的强大之处:它们让我们能够以声明式的方式为代码添加横切关注点,如日志记录、性能监控、缓存、验证等,而不需要修改业务逻辑代码本身。
4. 声明合并与类型扩展 🔗
4.1 接口合并
什么是声明合并?
声明合并是TypeScript的一个强大特性,它允许我们将多个同名的声明合并成一个单一的定义。最常见的是接口合并,这让我们可以在不同的地方扩展同一个接口。
接口合并示例:
// 基础用户接口
interface User {
id: number
name: string
}
// 在另一个地方扩展用户接口
interface User {
email: string
createdAt: Date
}
// 再次扩展
interface User {
isActive: boolean
}
// 现在User接口包含所有属性
const user: User = {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
createdAt: new Date(),
isActive: true
}
4.2 模块扩展
扩展内置对象:
// 扩展String对象
declare global {
interface String {
toCapitalize(): string
}
}
String.prototype.toCapitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1).toLowerCase()
}
// 使用扩展方法
const text = 'hello world'
console.log(text.toCapitalize()) // "Hello world"
通过声明合并,我们可以灵活地扩展现有的类型定义,这在处理第三方库或内置对象时特别有用。
5. 枚举的艺术 🎯
5.1 枚举的设计理念与价值
什么是枚举?为什么它如此重要?
枚举(Enumeration,简称 Enum)是一种用于定义命名常量集合的数据类型。在软件开发中,我们经常需要表示一组相关的常量值,比如星期几、月份、状态码、颜色等。枚举为这些常量提供了类型安全、可读性强的解决方案。
枚举解决的核心问题:
在没有枚举的时代,开发者通常使用"魔法数字"或"魔法字符串"来表示常量:
-
魔法数字问题:
// 难以理解的魔法数字 if (userRole === 1) { /* 1代表什么?管理员?普通用户? */ } if (orderStatus === 3) { /* 3是什么状态? */ } -
魔法字符串问题:
// 容易出错的魔法字符串 if (theme === "dark") { /* 如果拼写错误怎么办? */ } if (status === "in_progress") { /* 下划线还是驼峰? */ } -
维护困难:
- 常量分散在代码各处,难以统一管理
- 修改常量值时需要全局搜索替换
- 没有编译时检查,容易出现运行时错误
枚举的设计价值:
枚举的出现解决了这些问题,并带来了额外的价值:
- 语义化:用有意义的名称代替难以理解的数字或字符串
- 类型安全:编译时检查,防止使用无效值
- 智能提示:IDE 可以提供准确的代码补全
- 集中管理:相关常量统一定义,便于维护
- 文档化:枚举本身就是很好的文档说明
枚举在不同编程语言中的演进:
- C语言时代:简单的整数常量集合
- Java/C#时代:面向对象的枚举,支持方法和属性
- TypeScript时代:结合了类型安全和灵活性
TypeScript 枚举的特殊优势:
TypeScript 的枚举系统比许多其他语言更加灵活和强大:
- 多种类型支持:数字枚举、字符串枚举、异构枚举
- 编译时优化:const 枚举可以在编译时内联
- 反向映射:数字枚举支持从值到名称的反向查找
- 类型推断:与 TypeScript 类型系统完美集成
让我们通过对比来看看枚举的价值:
没有枚举的痛苦:
// 不好的做法:使用魔法数字
const ORDER_PENDING = 0
const ORDER_CONFIRMED = 1
const ORDER_SHIPPED = 2
const ORDER_DELIVERED = 3
const ORDER_CANCELLED = 4
// 问题1:数字没有语义,难以理解
function processOrder(status: number) {
if (status === 1) { /* 1代表什么状态?需要查文档或常量定义 */ }
}
// 问题2:容易传入无效值
processOrder(999) // 编译通过,但999不是有效的订单状态
// 问题3:没有智能提示
// IDE 无法知道应该传入什么值
有了枚举的清晰:
// 好的做法:使用枚举
enum OrderStatus {
Pending, // 0 - 等待处理
Confirmed, // 1 - 已确认
Shipped, // 2 - 已发货
Delivered, // 3 - 已送达
Cancelled // 4 - 已取消
}
// 优势1:语义清晰,一目了然
function processOrder(status: OrderStatus) {
if (status === OrderStatus.Confirmed) { /* 清楚地知道这是确认状态 */ }
}
// 优势2:类型安全,防止无效值
processOrder(OrderStatus.Pending) // ✅ 正确
// processOrder(999) // ❌ 编译错误:类型不匹配
// 优势3:智能提示
// IDE 会提示所有可用的枚举值
枚举的应用场景:
- 状态管理:订单状态、用户状态、任务状态等
- 配置选项:主题设置、语言选择、权限级别等
- 常量定义:HTTP 状态码、错误代码、事件类型等
- 业务规则:优先级、分类、类型等
枚举设计的最佳实践:
-
命名规范:
- 枚举名使用 PascalCase(如
OrderStatus) - 枚举值使用 PascalCase(如
Pending) - 保持命名的一致性和语义性
- 枚举名使用 PascalCase(如
-
值的选择:
- 数字枚举:适用于内部逻辑,性能优先
- 字符串枚举:适用于外部交互,可读性优先
- 避免混合类型,保持一致性
-
扩展性考虑:
- 预留扩展空间
- 考虑向后兼容性
- 避免删除或重新排序现有值
通过深入理解枚举的设计理念和价值,我们可以更好地利用这个工具来编写更加清晰、安全、可维护的代码。
5.2 数字枚举
数字枚举是最基本的枚举类型,成员会被自动赋予递增的数字值:
// 基础数字枚举
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
// 自定义起始值
enum HttpStatus {
OK = 200,
NotFound = 404,
InternalServerError = 500
}
// 部分初始化(后续成员会自动递增)
enum Priority {
Low = 1, // 1
Medium, // 2
High, // 3
Critical = 10, // 10
Emergency // 11
}
// 使用数字枚举
function move(direction: Direction): void {
switch (direction) {
case Direction.Up:
console.log('向上移动')
break
case Direction.Down:
console.log('向下移动')
break
case Direction.Left:
console.log('向左移动')
break
case Direction.Right:
console.log('向右移动')
break
}
}
move(Direction.Up) // "向上移动"
move(0) // 也可以直接使用数字值(不推荐)
数字枚举的反向映射:
enum Color {
Red,
Green,
Blue
}
console.log(Color.Red) // 0
console.log(Color[0]) // "Red"
console.log(Color[1]) // "Green"
// 枚举对象的实际结构
console.log(Color)
// {
// 0: "Red",
// 1: "Green",
// 2: "Blue",
// Red: 0,
// Green: 1,
// Blue: 2
// }
5.3 字符串枚举
字符串枚举的每个成员都必须用字符串字面量初始化:
// 字符串枚举
enum LogLevel {
ERROR = 'error',
WARN = 'warn',
INFO = 'info',
DEBUG = 'debug'
}
enum Theme {
LIGHT = 'light',
DARK = 'dark',
AUTO = 'auto'
}
// 使用字符串枚举
function log(level: LogLevel, message: string): void {
console.log(`[${level.toUpperCase()}] ${message}`)
}
log(LogLevel.ERROR, '这是一个错误信息') // "[ERROR] 这是一个错误信息"
// 字符串枚举的优势:更好的调试体验
const currentTheme = Theme.DARK
console.log(currentTheme) // "dark"(而不是数字)
5.4 异构枚举(混合枚举)
异构枚举包含数字和字符串成员:
// 异构枚举(不推荐,除非有特殊需求)
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
// 更实际的例子:API响应状态
enum ApiResponse {
SUCCESS = 200,
ERROR = 'ERROR',
LOADING = 'LOADING',
NOT_FOUND = 404
}
5.5 计算成员和常量成员
枚举成员可以是常量或计算值:
// 常量成员
enum FileAccess {
// 常量成员
None,
Read = 1 << 1, // 计算值:2
Write = 1 << 2, // 计算值:4
ReadWrite = Read | Write, // 计算值:6
}
// 计算成员
enum E {
A = getSomeValue(), // 计算成员
B, // 错误!计算成员后面必须有初始化器
}
function getSomeValue(): number {
return 1
}
// 实际应用:权限系统
enum Permission {
READ = 1, // 001
WRITE = 2, // 010
EXECUTE = 4, // 100
READ_WRITE = READ | WRITE, // 011
READ_EXECUTE = READ | EXECUTE, // 101
WRITE_EXECUTE = WRITE | EXECUTE, // 110
ALL = READ | WRITE | EXECUTE // 111
}
function hasPermission(userPermission: number, required: Permission): boolean {
return (userPermission & required) === required
}
const userPerm = Permission.READ_WRITE
console.log(hasPermission(userPerm, Permission.READ)) // true
console.log(hasPermission(userPerm, Permission.EXECUTE)) // false
5.6 const 枚举
const 枚举在编译时会被完全删除,成员会被内联:
// 普通枚举
enum RegularEnum {
A,
B,
C
}
// const 枚举
const enum ConstEnum {
A,
B,
C
}
// 编译后的代码
let regularValue = RegularEnum.A // 编译为:RegularEnum.A
let constValue = ConstEnum.A // 编译为:0(直接内联)
// const 枚举的优势:更小的代码体积,更好的性能
const enum Direction {
Up,
Down,
Left,
Right
}
function move(direction: Direction): void {
// 编译时这里会直接使用数字值
if (direction === Direction.Up) { // 编译为:if (direction === 0)
console.log('向上移动')
}
}
5.7 外部枚举
外部枚举用于描述已经存在的枚举类型:
// 外部枚举声明(通常在 .d.ts 文件中)
declare enum ExternalEnum {
A = 1,
B,
C = 2
}
// 外部 const 枚举
declare const enum ExternalConstEnum {
A,
B,
C
}
5.8 枚举的实际应用
游戏状态管理:
enum GameState {
MENU = 'menu',
PLAYING = 'playing',
PAUSED = 'paused',
GAME_OVER = 'game_over'
}
class Game {
private state: GameState = GameState.MENU
public setState(newState: GameState): void {
console.log(`游戏状态从 ${this.state} 变更为 ${newState}`)
this.state = newState
}
public getState(): GameState {
return this.state
}
public canPause(): boolean {
return this.state === GameState.PLAYING
}
}
HTTP 状态码管理:
enum HttpStatusCode {
// 成功状态
OK = 200,
CREATED = 201,
NO_CONTENT = 204,
// 重定向状态
MOVED_PERMANENTLY = 301,
FOUND = 302,
// 客户端错误
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
// 服务器错误
INTERNAL_SERVER_ERROR = 500,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503
}
class ApiResponse<T> {
constructor(
public status: HttpStatusCode,
public data?: T,
public message?: string
) {}
public isSuccess(): boolean {
return this.status >= 200 && this.status < 300
}
public isClientError(): boolean {
return this.status >= 400 && this.status < 500
}
public isServerError(): boolean {
return this.status >= 500
}
}
// 使用示例
const response = new ApiResponse(HttpStatusCode.NOT_FOUND, null, '用户不存在')
console.log(response.isClientError()) // true
枚举的最佳实践:
- 优先使用字符串枚举:更好的调试体验和序列化支持
- 使用 const 枚举优化性能:在不需要反向映射时
- 避免异构枚举:除非有特殊业务需求
- 使用有意义的命名:让代码自文档化
- 考虑使用联合类型替代:在某些场景下更灵活
// 联合类型替代枚举的例子
type Theme = 'light' | 'dark' | 'auto' // 更简洁,类型安全
const theme: Theme = 'dark'
6. 混入(Mixins)模式 🎭
6.1 什么是混入?
混入(Mixin)是一种设计模式,让我们可以将多个类的功能组合到一个类中。就像制作奶茶一样,你可以选择茶底(红茶、绿茶),然后加入不同的配料(珍珠、椰果、布丁)。
基础混入示例:
// 基础类型定义
type Constructor = new (...args: any[]) => {}
// 时间戳混入 - 给类添加时间追踪功能
function WithTimestamp<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date()
getAge(): number {
return Date.now() - this.createdAt.getTime()
}
}
}
// 基础用户类
class User {
constructor(public name: string) {}
greet(): string {
return `Hello, I'm ${this.name}`
}
}
// 使用混入创建增强的用户类
const TimestampedUser = WithTimestamp(User)
// 使用
const user = new TimestampedUser('张三')
console.log(user.greet()) // "Hello, I'm 张三"
console.log(user.getAge()) // 返回创建后经过的毫秒数
通过混入,我们可以灵活地组合不同的功能,创建具有多种能力的类。
7. 实战案例:企业级任务管理系统 🏢
7.1 系统架构设计
让我们综合运用前面学到的所有知识,构建一个完整的任务管理系统。这个系统将展示类、接口、枚举、装饰器、模块等特性的协同工作。
系统功能需求:
- 任务的增删改查
- 用户权限管理
- 任务状态流转
- 日志记录和性能监控
- 数据验证和缓存
7.2 核心类型定义
// 枚举定义
enum TaskStatus {
TODO = 'todo',
IN_PROGRESS = 'in_progress',
IN_REVIEW = 'in_review',
DONE = 'done',
CANCELLED = 'cancelled'
}
enum Priority {
LOW = 1,
MEDIUM = 2,
HIGH = 3,
URGENT = 4
}
enum UserRole {
ADMIN = 'admin',
MANAGER = 'manager',
DEVELOPER = 'developer',
VIEWER = 'viewer'
}
// 基础接口
interface IUser {
id: string
username: string
email: string
role: UserRole
createdAt: Date
}
interface ITask {
id: string
title: string
description?: string
status: TaskStatus
priority: Priority
assigneeId?: string
creatorId: string
createdAt: Date
updatedAt: Date
dueDate?: Date
}
interface IComment {
id: string
taskId: string
userId: string
content: string
createdAt: Date
}
// 权限接口
interface IPermissionChecker {
canCreateTask(user: IUser): boolean
canUpdateTask(user: IUser, task: ITask): boolean
canDeleteTask(user: IUser, task: ITask): boolean
canViewTask(user: IUser, task: ITask): boolean
}
7.3 装饰器定义
// 日志装饰器
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
console.log(`[${new Date().toISOString()}] 调用方法: ${target.constructor.name}.${propertyKey}`)
console.log(`参数:`, args)
const result = originalMethod.apply(this, args)
console.log(`返回结果:`, result)
return result
}
return descriptor
}
// 权限检查装饰器
function requirePermission(permission: keyof IPermissionChecker) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (user: IUser, ...args: any[]) {
const permissionChecker: IPermissionChecker = this.permissionChecker
if (!permissionChecker[permission](user, ...args)) {
throw new Error(`用户 ${user.username} 没有执行 ${propertyKey} 的权限`)
}
return originalMethod.apply(this, [user, ...args])
}
return descriptor
}
}
// 缓存装饰器
function cache(ttl: number = 60000) { // 默认缓存1分钟
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
const cacheMap = new Map<string, { data: any; timestamp: number }>()
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args)
const cached = cacheMap.get(key)
if (cached && Date.now() - cached.timestamp < ttl) {
console.log(`缓存命中: ${propertyKey}`)
return cached.data
}
const result = originalMethod.apply(this, args)
cacheMap.set(key, { data: result, timestamp: Date.now() })
console.log(`缓存存储: ${propertyKey}`)
return result
}
return descriptor
}
}
// 验证装饰器
function validate(validator: (value: any) => boolean, message: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
for (const arg of args) {
if (!validator(arg)) {
throw new Error(`验证失败: ${message}`)
}
}
return originalMethod.apply(this, args)
}
return descriptor
}
}
7.4 权限管理系统
class PermissionChecker implements IPermissionChecker {
canCreateTask(user: IUser): boolean {
return [UserRole.ADMIN, UserRole.MANAGER, UserRole.DEVELOPER].includes(user.role)
}
canUpdateTask(user: IUser, task: ITask): boolean {
if (user.role === UserRole.ADMIN) return true
if (user.role === UserRole.MANAGER) return true
if (user.role === UserRole.DEVELOPER && task.assigneeId === user.id) return true
return false
}
canDeleteTask(user: IUser, task: ITask): boolean {
if (user.role === UserRole.ADMIN) return true
if (user.role === UserRole.MANAGER && task.creatorId === user.id) return true
return false
}
canViewTask(user: IUser, task: ITask): boolean {
// 所有用户都可以查看任务
return true
}
}
7.5 用户管理系统
class User implements IUser {
public readonly id: string
public readonly createdAt: Date
constructor(
public username: string,
public email: string,
public role: UserRole
) {
this.id = this.generateId()
this.createdAt = new Date()
}
private generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
public hasRole(role: UserRole): boolean {
return this.role === role
}
public isAdmin(): boolean {
return this.role === UserRole.ADMIN
}
public canManage(): boolean {
return [UserRole.ADMIN, UserRole.MANAGER].includes(this.role)
}
}
class UserManager {
private users: Map<string, User> = new Map()
@log
createUser(username: string, email: string, role: UserRole): User {
const user = new User(username, email, role)
this.users.set(user.id, user)
return user
}
@cache(30000) // 缓存30秒
getUserById(id: string): User | undefined {
return this.users.get(id)
}
@cache(30000)
getUserByUsername(username: string): User | undefined {
return Array.from(this.users.values()).find(user => user.username === username)
}
getAllUsers(): User[] {
return Array.from(this.users.values())
}
}
7.6 任务管理核心系统
class Task implements ITask {
public readonly id: string
public readonly createdAt: Date
public updatedAt: Date
constructor(
public title: string,
public creatorId: string,
public priority: Priority = Priority.MEDIUM,
public description?: string,
public assigneeId?: string,
public dueDate?: Date
) {
this.id = this.generateId()
this.createdAt = new Date()
this.updatedAt = new Date()
this.status = TaskStatus.TODO
}
public status: TaskStatus = TaskStatus.TODO
private generateId(): string {
return 'task_' + Date.now().toString(36) + Math.random().toString(36).substr(2)
}
public updateStatus(newStatus: TaskStatus): void {
this.status = newStatus
this.updatedAt = new Date()
}
public assign(userId: string): void {
this.assigneeId = userId
this.updatedAt = new Date()
}
public isOverdue(): boolean {
return this.dueDate ? new Date() > this.dueDate : false
}
public getDaysUntilDue(): number | null {
if (!this.dueDate) return null
const timeDiff = this.dueDate.getTime() - new Date().getTime()
return Math.ceil(timeDiff / (1000 * 3600 * 24))
}
}
class TaskManager {
private tasks: Map<string, Task> = new Map()
private permissionChecker: IPermissionChecker
constructor(permissionChecker: IPermissionChecker) {
this.permissionChecker = permissionChecker
}
@log
@requirePermission('canCreateTask')
@validate(task => task.title && task.title.trim().length > 0, '任务标题不能为空')
createTask(
user: IUser,
title: string,
priority: Priority = Priority.MEDIUM,
description?: string,
assigneeId?: string,
dueDate?: Date
): Task {
const task = new Task(title, user.id, priority, description, assigneeId, dueDate)
this.tasks.set(task.id, task)
console.log(`任务创建成功: ${title} (ID: ${task.id})`)
return task
}
@log
@requirePermission('canUpdateTask')
updateTaskStatus(user: IUser, taskId: string, newStatus: TaskStatus): Task {
const task = this.getTaskById(taskId)
if (!task) {
throw new Error(`任务不存在: ${taskId}`)
}
const oldStatus = task.status
task.updateStatus(newStatus)
console.log(`任务状态更新: ${taskId} ${oldStatus} -> ${newStatus}`)
return task
}
@log
@requirePermission('canDeleteTask')
deleteTask(user: IUser, taskId: string): boolean {
const task = this.getTaskById(taskId)
if (!task) {
throw new Error(`任务不存在: ${taskId}`)
}
const deleted = this.tasks.delete(taskId)
if (deleted) {
console.log(`任务删除成功: ${taskId}`)
}
return deleted
}
@cache(10000) // 缓存10秒
getTaskById(taskId: string): Task | undefined {
return this.tasks.get(taskId)
}
@cache(5000)
getTasksByStatus(status: TaskStatus): Task[] {
return Array.from(this.tasks.values()).filter(task => task.status === status)
}
@cache(5000)
getTasksByAssignee(assigneeId: string): Task[] {
return Array.from(this.tasks.values()).filter(task => task.assigneeId === assigneeId)
}
@cache(5000)
getOverdueTasks(): Task[] {
return Array.from(this.tasks.values()).filter(task => task.isOverdue())
}
getTaskStatistics(): Record<TaskStatus, number> {
const stats: Record<TaskStatus, number> = {
[TaskStatus.TODO]: 0,
[TaskStatus.IN_PROGRESS]: 0,
[TaskStatus.IN_REVIEW]: 0,
[TaskStatus.DONE]: 0,
[TaskStatus.CANCELLED]: 0
}
for (const task of this.tasks.values()) {
stats[task.status]++
}
return stats
}
}
7.7 系统集成和使用示例
// 系统初始化
const permissionChecker = new PermissionChecker()
const userManager = new UserManager()
const taskManager = new TaskManager(permissionChecker)
// 创建用户
const admin = userManager.createUser('admin', 'admin@company.com', UserRole.ADMIN)
const manager = userManager.createUser('manager', 'manager@company.com', UserRole.MANAGER)
const dev1 = userManager.createUser('developer1', 'dev1@company.com', UserRole.DEVELOPER)
const dev2 = userManager.createUser('developer2', 'dev2@company.com', UserRole.DEVELOPER)
// 创建任务
console.log('=== 创建任务 ===')
const task1 = taskManager.createTask(
manager,
'实现用户登录功能',
Priority.HIGH,
'需要实现JWT认证和权限验证',
dev1.id,
new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7天后到期
)
const task2 = taskManager.createTask(
manager,
'优化数据库查询性能',
Priority.MEDIUM,
'分析慢查询并优化索引',
dev2.id
)
const task3 = taskManager.createTask(
admin,
'制定代码规范文档',
Priority.LOW,
'建立团队统一的代码规范'
)
// 更新任务状态
console.log('\n=== 更新任务状态 ===')
taskManager.updateTaskStatus(dev1, task1.id, TaskStatus.IN_PROGRESS)
taskManager.updateTaskStatus(dev2, task2.id, TaskStatus.IN_PROGRESS)
taskManager.updateTaskStatus(admin, task3.id, TaskStatus.DONE)
// 查询任务
console.log('\n=== 查询任务 ===')
console.log('进行中的任务:', taskManager.getTasksByStatus(TaskStatus.IN_PROGRESS))
console.log('开发者1的任务:', taskManager.getTasksByAssignee(dev1.id))
console.log('任务统计:', taskManager.getTaskStatistics())
// 权限测试
console.log('\n=== 权限测试 ===')
try {
// 开发者尝试删除不属于自己的任务(应该失败)
taskManager.deleteTask(dev1, task2.id)
} catch (error) {
console.log('权限验证成功:', error.message)
}
// 管理员删除任务(应该成功)
taskManager.deleteTask(admin, task3.id)
7.8 系统特性总结
这个任务管理系统展示了以下 TypeScript 特性的综合应用:
1. 类与继承:
User和Task类封装了业务逻辑- 清晰的属性访问控制(public、private、readonly)
2. 接口设计:
IUser、ITask、IPermissionChecker定义了清晰的契约- 接口实现确保了类型安全
3. 枚举应用:
TaskStatus、Priority、UserRole提供了类型安全的常量- 避免了魔法字符串和数字
4. 装饰器功能:
@log提供了方法调用日志@requirePermission实现了权限控制@cache提供了缓存功能@validate实现了参数验证
5. 模块化设计:
- 清晰的职责分离
- 易于测试和维护
- 可扩展的架构
6. 类型安全:
- 编译时类型检查
- 防止运行时错误
- 良好的IDE支持
这个系统演示了如何在实际项目中合理运用 TypeScript 的各种特性,创建出既类型安全又易于维护的企业级应用。
8. 学习总结 📚
通过第三天的深度学习,我们掌握了 TypeScript 的核心面向对象特性:
🏗️ 类系统核心概念:
- 基础类定义:属性、方法、构造函数的使用
- 访问修饰符:public、private、protected 的正确使用
- 静态成员:类级别的属性和方法
- 参数属性:构造函数参数的简化写法
- 继承与多态:代码复用和接口统一的实现
- 抽象类:为相关类提供共同基础结构
- 接口实现:定义类必须遵循的契约
- 属性访问器:getter/setter 的使用场景
📦 模块系统精髓:
- ES6 模块:import/export 的各种用法
- 重新导出:创建统一的模块入口点
- 动态导入:按需加载和代码分割
- 模块解析:相对导入 vs 非相对导入
- 路径映射:简化导入路径的配置
- 命名空间:全局代码组织的传统方式
✨ 装饰器的威力:
- 方法装饰器:增强方法功能(日志、缓存、重试等)
- 类装饰器:修改类的行为(单例、自动绑定等)
- 属性装饰器:控制属性访问(验证、格式化等)
- 参数装饰器:参数级别的验证和处理
- 装饰器工厂:可配置的装饰器创建
- 装饰器组合:多个装饰器的协同工作
🎯 枚举的艺术:
- 数字枚举:自动递增的数字常量
- 字符串枚举:更好的调试体验
- const 枚举:编译时优化
- 计算成员:动态计算的枚举值
- 枚举的最佳实践:何时使用枚举 vs 联合类型
🔗 声明合并与扩展:
- 接口合并:多个同名接口的自动合并
- 模块扩展:扩展现有模块的功能
- 全局扩展:为内置对象添加新方法
🎭 混入模式:
- 多重继承的实现:组合多个类的功能
- 类型约束:确保混入的类型安全
通过第三天的深入学习,我们已经掌握了 TypeScript 面向对象编程的核心概念和实际应用。这些知识将为构建大型、可维护的 TypeScript 应用程序奠定坚实的基础。