类、抽象类与面向对象建模TypeScript 不是一门纯粹的面向对象语言但它对类系统的支持足够完整足以覆盖很多工程场景。问题在于很多人学到class之后会误以为这就是组织 TypeScript 代码的默认方式。现实恰恰相反类是重要工具但不是默认答案。这一篇的重点不是带你机械复习“什么是封装、继承、多态”而是更现实地回答三个问题TypeScript 里的类到底能做什么抽象类和接口分别适合什么位置在现代 JavaScript/TypeScript 工程里什么时候该用类什么时候不该用类首先是一种“带状态和行为的实例模型”最基本的类写法如下classUser{constructor(publicid:number,publicname:string){}greet(){returnHello,${this.name};}}这里的public id: number和public name: string是一种语法简写表示在构造函数接收参数同时把它们声明成实例属性这使得类特别适合表达“一个对象既有数据又有围绕这些数据运作的方法”的场景。类和普通对象最大的区别不在语法而在实例语义普通对象很适合表示数据constuser{id:1,name:Alice};而类更适合表示“可被实例化、可带行为、可能有生命周期”的实体classTimer{privatestartTimeDate.now();getElapsedTime(){returnDate.now()-this.startTime;}}所以你可以把类理解成一种更强的组织方式但前提是你真的需要这种“实例化对象 行为封装”的模型。访问修饰符让类更适合做封装TypeScript 支持这些访问修饰符publicprivateprotectedreadonly看一个例子classAccount{publicowner:string;privatebalance:number;constructor(owner:string,balance:number){this.ownerowner;this.balancebalance;}deposit(amount:number){this.balanceamount;}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日于上海。
【Typescript】11-类抽象类与面向对象建模
发布时间:2026/5/23 0:07:22
类、抽象类与面向对象建模TypeScript 不是一门纯粹的面向对象语言但它对类系统的支持足够完整足以覆盖很多工程场景。问题在于很多人学到class之后会误以为这就是组织 TypeScript 代码的默认方式。现实恰恰相反类是重要工具但不是默认答案。这一篇的重点不是带你机械复习“什么是封装、继承、多态”而是更现实地回答三个问题TypeScript 里的类到底能做什么抽象类和接口分别适合什么位置在现代 JavaScript/TypeScript 工程里什么时候该用类什么时候不该用类首先是一种“带状态和行为的实例模型”最基本的类写法如下classUser{constructor(publicid:number,publicname:string){}greet(){returnHello,${this.name};}}这里的public id: number和public name: string是一种语法简写表示在构造函数接收参数同时把它们声明成实例属性这使得类特别适合表达“一个对象既有数据又有围绕这些数据运作的方法”的场景。类和普通对象最大的区别不在语法而在实例语义普通对象很适合表示数据constuser{id:1,name:Alice};而类更适合表示“可被实例化、可带行为、可能有生命周期”的实体classTimer{privatestartTimeDate.now();getElapsedTime(){returnDate.now()-this.startTime;}}所以你可以把类理解成一种更强的组织方式但前提是你真的需要这种“实例化对象 行为封装”的模型。访问修饰符让类更适合做封装TypeScript 支持这些访问修饰符publicprivateprotectedreadonly看一个例子classAccount{publicowner:string;privatebalance:number;constructor(owner:string,balance:number){this.ownerowner;this.balancebalance;}deposit(amount:number){this.balanceamount;}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日于上海。