应优先选择组合而非原型继承,因组合更符合“有一个”语义、支持运行时替换、隐藏实现细节、降低演进风险;仅当明确存在“是一种”关系且需多态时才用原型继承。

原型继承和组合继承不是同一层面的概念——原型继承是 JavaScript 中基于原型链的对象创建机制,属于语言底层特性;而“组合继承”其实是对「构造函数继承 + 原型链继承」的混合用法,是一种人为设计的模拟类式继承的模式。真正需要对比决策的,是基于原型的复用方式(如 Object.create、class extends)与基于组合的设计方式(即通过对象持有并委托行为,而非建立父子关系)。关键不在“怎么写语法”,而在“要不要建类型层级”。
看语义关系:是“一种”还是“有一个”?
这是最直观的第一筛。
- 如果 A 是 B 的一种具体形式(比如 Penguin 是 Bird),且业务上明确需要多态(如统一调用 .fly(),哪怕部分子类重写为抛异常),才考虑原型继承(
class Penguin extends Bird) - 如果 A 包含 B 的能力,但 B 不是 A 的子类型(比如 OrderService 有 PaymentProcessor),那就用组合:定义字段 + 委托调用,不产生类型继承链
- 注意:语义成立 ≠ 就该继承。比如 “Bird has feathers” 是组合,“Bird is Animal” 才是继承候选
看可变性需求:运行时能否换实现?
组合天然支持动态替换,原型继承则绑定死在构造链里。
- 需要测试时注入 mock(如用
MockPaymentProcessor替换真实支付器)→ 必须组合 - 依赖可能随配置切换(如开发用内存缓存、生产用 RedisClient)→ 组合 + 接口抽象更稳妥
- 若所有实现完全固定、永不更换,且无测试隔离诉求,原型继承语法更简洁
看父类是否“设计为被继承”?
很多类只是普通对象,不是“可扩展基类”。强行继承会踩坑。
- 检查是否有文档说明“可子类化”、是否提供 protected 钩子、是否有 final 方法或隐式契约(如某方法必须先调 init())
- 父类字段是 public/protected?继承会暴露内部细节;组合可通过私有字段 + 明确接口隐藏实现
- 典型反例:早期 JDK 的
Stack extends Vector—— Stack 只需用到 add/remove,却被迫继承全部 Vector 行为和扩容逻辑,违反单一职责
看系统长期演进成本
继承结构越深,修改风险越高;组合结构天然扁平,扩展更安全。
- 新增一个“会游泳但不会飞的鸟”,继承体系要拆抽象层(Flyable/Unflyable)、改多个子类;组合只需新写一个 SwimBehavior 并赋给对应实例
- 父类某天删掉一个非 final 方法,所有子类编译/运行都可能崩;组合对象只依赖公开方法签名,只要接口不变就稳
- 团队协作中,组合让每个模块职责清晰;继承容易导致“谁都能改父类,谁都得看懂整个链”











