【Typescript】11-类抽象类与面向对象建模
类、抽象类与面向对象建模
TypeScript 不是一门纯粹的面向对象语言,但它对类系统的支持足够完整,足以覆盖很多工程场景。问题在于,很多人学到class之后,会误以为这就是组织 TypeScript 代码的默认方式。现实恰恰相反:类是重要工具,但不是默认答案。
这一篇的重点,不是带你机械复习“什么是封装、继承、多态”,而是更现实地回答三个问题:
- TypeScript 里的类到底能做什么
- 抽象类和接口分别适合什么位置
- 在现代 JavaScript/TypeScript 工程里,什么时候该用类,什么时候不该用
类首先是一种“带状态和行为的实例模型”
最基本的类写法如下:
classUser{constructor(publicid:number,publicname:string){}greet(){return`Hello,${this.name}`;}}这里的public id: number和public name: string是一种语法简写,表示:
- 在构造函数接收参数
- 同时把它们声明成实例属性
这使得类特别适合表达“一个对象既有数据,又有围绕这些数据运作的方法”的场景。
类和普通对象最大的区别,不在语法,而在实例语义
普通对象很适合表示数据:
constuser={id:1,name:"Alice"};而类更适合表示“可被实例化、可带行为、可能有生命周期”的实体:
classTimer{privatestartTime=Date.now();getElapsedTime(){returnDate.now()-this.startTime;}}所以你可以把类理解成一种更强的组织方式,但前提是你真的需要这种“实例化对象 + 行为封装”的模型。
访问修饰符让类更适合做封装
TypeScript 支持这些访问修饰符:
publicprivateprotectedreadonly
看一个例子:
classAccount{publicowner:string;privatebalance:number;constructor(owner:string,balance:number){this.owner=owner;this.balance=balance;}deposit(amount:number){this.balance+=amount;}getBalance(){returnthis.balance;}}这里:
owner可以在外部访问balance只能在类内部访问
private的价值不只是“防止乱改”,更是让你能够清晰地区分:
- 哪些是对外暴露的稳定接口
- 哪些是内部实现细节
这对于 SDK、服务对象、领域模型非常重要。
protected适合“对子类开放、对外部隐藏”
classBaseService{protectedlog(message:string){console.log("[service]",message);}}classUserServiceextendsBaseService{createUser(){this.log("create user");}}protected允许子类使用,但外部实例不能直接访问。这类能力在可继承的基类设计里很常见。
readonly能表达对象生命周期中的不变部分
classOrder{constructor(publicreadonlyid:string,publicstatus:"pending"|"paid"){}}这里的id一旦构造完成就不允许再变。这个约束非常贴近业务语义,也比靠注释说“不要改它”更可靠。
实现接口:把能力边界和实现细节分开
interfaceLogger{log(message:string):void;}classConsoleLoggerimplementsLogger{log(message:string){console.log(message);}}这里接口和类的分工非常清楚:
- 接口定义“应该具备什么能力”
- 类负责“具体怎么实现”
这种模式在这些场景中非常常见:
- 依赖注入
- 策略模式
- 适配器模式
- 可替换服务实现
如果你希望系统依赖的是能力,而不是某个具体类,接口就很重要。
抽象类:适合“部分规则固定,部分细节留给子类”
抽象类和接口容易被混淆,但它们解决的问题不完全一样。看一个例子:
abstractclassAnimal{abstractspeak():void;move(){console.log("moving");}}classDogextendsAnimal{speak(){console.log("wang");}}这里:
Animal不能直接被实例化- 它提供了通用实现
move() - 同时强制子类实现
speak()
你可以把抽象类理解成“半成品基类”。它适合那些:
- 某些流程是固定的
- 但某些具体步骤必须交给不同子类补齐
接口和抽象类怎么选
一个简单的判断标准:
- 如果你只想描述能力契约,用接口
- 如果你还想提供一部分共享实现,用抽象类
接口更轻,耦合更低;抽象类更强,但也更容易把继承树绑得太紧。工程上通常是优先接口,只有当共享实现真的有价值时,再考虑抽象类。
继承不是错,但通常不是第一选择
现代 JavaScript/TypeScript 社区对继承更谨慎,不是因为继承不能用,而是因为很多系统最后都会被深层继承链搞得越来越难维护。
例如:
- 父类改一点,多个子类都受影响
- 子类不小心依赖了父类内部实现细节
- 行为分发越来越难追踪
所以比起“默认继承”,很多项目更倾向于:
- 对象组合
- 函数式封装
- 接口 + 多实现
这类方式通常更灵活,也更适合现代前端和 Node.js 工程。
类什么时候真的有优势
类最适合这些场景:
- SDK 封装
- 带内部状态的服务对象
- 插件系统
- 有生命周期管理的对象
- 明确存在多种实现并共享部分基础逻辑的体系
例如数据库客户端、消息队列客户端、缓存实例、编辑器插件体系,都很适合类。
类什么时候反而会让事情更糟
如果你只是想表达一份数据,或者封装一段纯函数逻辑,类很可能会引入额外复杂度。
例如:
- 一个简单工具函数库
- 一组纯数据转换逻辑
- 一个没有实例状态的工具模块
这些场景里,普通函数和对象字面量往往更清晰。
一个更现实的 TypeScript 观点
学类系统当然重要,但更重要的是别被“类就是高级架构”的幻觉带偏。真正成熟的工程判断不是“我会不会写 class”,而是“这段问题到底需要实例、封装、继承、多态中的哪一种能力”。
如果答案都不需要,那就别强上类。
本文小结
TypeScript 的类系统足够强,抽象类、访问修饰符、接口实现都能支持你完成相当完整的面向对象建模。但类不是默认模板,而是一种在合适场景下非常有效的组织工具。真正好的设计,不是让代码更像教科书里的 OOP,而是让业务边界更清楚、职责更稳定、扩展更自然。
练习
- 写一个
Shape抽象类,包含getArea()抽象方法和一个通用的printName()方法。 - 用接口定义
Payment,然后实现WechatPayment和AlipayPayment两个类,并比较接口与抽象类在这个场景中的区别。 - 选择你项目里的一个工具模块,思考它究竟更适合普通函数还是类,说明理由。
后记
2026年5月22日于上海。
